残念ながら、同僚のほとんど全員が
LRUとは何か、特定のサイズのキャッシュを実装する方法を知らないことに再び気付きました。 そのため、
LRUメソッドを迅速に実装する方法を説明する短い記事を書くことにし、同僚がキャッシュを不要な場所に手動でリセットすることを強制しないようにしました。
キャッシングは、一部のクエリに応じて計算結果を保存するものとして理解します。 つまり、繰り返されるクエリ結果は常に再計算されるわけではありませんが、キャッシュと呼ばれるテーブルから取得されることがあります。 現代のシステムにおけるキャッシングの役割を過大評価することは困難です。 これは、多くの場合、メモリ不足の問題を引き起こします。 実際、リクエストが多く、限られた数の結果を保存するだけのメモリがある場合はどうすればよいでしょうか? この場合、原則として、キャッシュは次のようにまっすぐになります。 キャッシュ
サイズは固定されており、
Nとし、結果は
N個の最も「人気のある」クエリに対してのみ保存されます。
つまり、計算の結果が保存され、再度要求される可能性があります。
これらの「人気のある」クエリを識別する方法は? 最も有名な方法は
LRUです。これについては、この記事で説明します。
LRU (最も最近使用されていない)は、最も長く要求されなかった値が置き換えられるアルゴリズムです。 したがって、最後のリクエストの時刻を値に保存する必要があります。 また、キャッシュされた値の数が
Nを超えるとすぐに、最も長く要求されていない値をキャッシュからプッシュする必要があります。
このメソッドを実装するには、2つのデータ構造が必要です。
- 直接キャッシュされた値を保存するhashTableハッシュテーブル。
- 優先キュー timeQueue 。 次の操作をサポートする構造:
- 値と優先度のペアtimeQueue.Add(val、priority)を追加します。
- 優先度が最も低いtimeQueue.extractMinValue()で値を取得(削除して返します) 。
優先キューの詳細については、
こちらをご覧ください。最初の計算では、
calculate(x)メソッドが使用されたと仮定します。
Calculateメソッドを新しい
CalculateWithCacheに置き換え
ます 。これにより、キャッシュが補充され、古い値がプッシュされ、キャッシュ内で見つからない場合に
Calculateから結果が要求されます。
これは、
calculateWithCacheアルゴリズムの動作方法です。
calculateWithCache(key) { curTime = getCurrentTime();
以上です。 ここで、ユーザーはキャッシュをフラッシュする代わりに、キャッシュサイズを設定する必要があります。 その際、適切なデフォルト値を設定することをお勧めします。
優先度キューの効果的な実装を使用する場合、
LRUを必要とするオーバーヘッドは
O(log N)です。
標準ライブラリでは、たとえばC ++で優先度キューを実装できます。 ただし、実装されていなくてもゆっくり読んでいる場合でも、バランスの取れたツリーを使用して、少し複雑ですが、同じ複雑さの優先度を持つキューを実装する方法を推測できます。
少し考えたい人への
質問 。 ハッシュテーブル操作の複雑さが一定であることを考慮して、一定のオーバーヘッドを達成する方法は?
ヒント:優先度キューを削除し、通常のキューを使用する必要があります:)
特定のキー
keyの計算時間
計算 、結果のボリューム、または他の何かを考慮に入れた、より複雑なヒューリスティックを見つけることができます。
しかし、ほとんどのタスクで、
LRUは最も「人気のある」クエリを最も適切に定義します。
注1 :保存される値の数ではなく、キャッシュごとのメモリ量を制限できます。 アルゴリズムは、実際には変更されず、長さの代わりに、新しい値を格納するためのメモリ+メモリが占有されます。
注2 :これはこの記事のトピックではないため、特にマルチスレッドの問題を回避しました。
更新 (コメントについてはToSHiC22に感謝)興味のある方のために、少し高度な
2Q実装へのリンク