クライアントクエリの結果を取得してキャッシュし、結果をキャッシュからクライアントに送信するアプリケーションがあります。
一度にキャッシュできるアイテムの数に制限があり、この制限を追跡すると、多数の同時リクエストを処理するときにアプリケーションのパフォーマンスが大幅に低下します。頻繁にロックせずにこの問題を解決し、パフォーマンスを向上させるより良い方法はありますか?
編集:私はCASアプローチを採用しましたが、うまく機能しているようです。
クライアントクエリの結果を取得してキャッシュし、結果をキャッシュからクライアントに送信するアプリケーションがあります。
一度にキャッシュできるアイテムの数に制限があり、この制限を追跡すると、多数の同時リクエストを処理するときにアプリケーションのパフォーマンスが大幅に低下します。頻繁にロックせずにこの問題を解決し、パフォーマンスを向上させるより良い方法はありますか?
編集:私はCASアプローチを採用しましたが、うまく機能しているようです。
まず、ロックを使用するのではなく、アトミック デクリメントと比較交換を使用してカウンターを操作します。この構文はコンパイラによって異なります。GCC では、次のようなことができます。
long remaining_cache_slots;
void release() {
__sync_add_and_fetch(&remaining_cache_slots, 1);
}
// Returns false if we've hit our cache limit
bool acquire() {
long prev_value, new_value;
do {
prev_value = remaining_cache_slots;
if (prev_value <= 0) return false;
new_value = prev_value - 1;
} while(!__sync_bool_compare_and_swap(&remaining_cache_slots, prev_value, new_value));
return true;
}
これにより、競合のウィンドウを減らすことができます。ただし、依然としてそのキャッシュ ラインがいたるところにバウンスすることになり、リクエスト レートが高いとパフォーマンスが大幅に低下する可能性があります。
ある程度の無駄を受け入れる場合 (つまり、キャッシュされた結果の数、または保留中の応答の数が制限をわずかに下回ることを許容する場合) には、いくつかの選択肢があります。1 つは、キャッシュをスレッド ローカルにすることです (設計で可能であれば)。もう 1 つは、使用する「キャッシュ トークン」のプールを各スレッドに予約させることです。
キャッシュ トークンのプールを予約するということは、各スレッドが N 個のエントリをキャッシュに挿入する権利を事前に予約できるということです。そのスレッドがキャッシュからエントリを削除すると、そのエントリがトークンのセットに追加されます。トークンが不足すると、グローバル プールからトークンを取得しようとし、トークンが多すぎる場合は、一部を元に戻します。コードは次のようになります。
long global_cache_token_pool;
__thread long thread_local_token_pool = 0;
// Release 10 tokens to the global pool when we go over 20
// The maximum waste for this scheme is 20 * nthreads
#define THREAD_TOKEN_POOL_HIGHWATER 20
#define THREAD_TOKEN_POOL_RELEASECT 10
// If we run out, acquire 5 tokens from the global pool
#define THREAD_TOKEN_POOL_ACQUIRECT 5
void release() {
thread_local_token_pool++;
if (thread_local_token_pool > THREAD_TOKEN_POOL_HIGHWATER) {
thread_local_token_pool -= THREAD_TOKEN_POOL_RELEASECT;
__sync_fetch_and_add(&global_token_pool, THREAD_TOKEN_POOL_RELEASECT);
}
}
bool acquire() {
if (thread_local_token_pool > 0) {
thread_local_token_pool--;
return true;
}
long prev_val, new_val, acquired;
do {
prev_val = global_token_pool;
acquired = std::min(THREAD_TOKEN_POOL_ACQUIRECT, prev_val);
if (acquired <= 0) return false;
new_val = prev_val - acquired;
} while (!__sync_bool_compare_and_swap(&remaining_cache_slots, prev_value, new_value));
thread_local_token_pool = acquired - 1;
return true;
}
このようにリクエストをバッチ処理すると、スレッドが共有データにアクセスする頻度が減るため、競合やキャッシュ チャーンの量が減ります。ただし、前述のように、制限の精度が少し低下するため、適切なバランスを得るには慎重に調整する必要があります。
で、結果を処理した後に 1 回だけSendResults
更新してみてください。totalResultsCached
これにより、ロックの取得/解放にかかる時間が最小限に抑えられます。
void SendResults( int resultsToSend, Request *request )
{
for (int i=0; i<resultsToSend; ++i)
{
send(request.remove())
}
lock totalResultsCached
totalResultsCached -= resultsToSend;
unlock totalResultsCached
}
通常 1 の場合resultsToSend
、私の提案は大きな違いはありません。
また、キャッシュ制限に達した後、各リクエストを送信した直後にが更新されないResultCallback
ため、余分なリクエストが でドロップされる場合があります。SendResults
totalResultsCached