LRUキャッシュの実装アルゴリズムについて議論しましょう

LRUキャッシュの実装アルゴリズムについて議論しましょう

ビジネスモデル

読み取り、書き込み、削除の比率はおよそ 7:3:1 です。少なくとも 500 万個のキャッシュ (キャッシュあたり平均 6k) をサポートする必要があり、比較的優れたパフォーマンスのキャッシュ アルゴリズムの設計が必要です。

アルゴリズム分析

MemCached や Velocity などの既存のキー値キャッシュ ソリューションを考慮せず、.NET gc を使用せずにメモリを個別に管理したり、ランダムおよびシーケンシャルなデータ読み取りのシナリオを考慮しなくても、現在、次の LRU キャッシュ ソリューションが利用可能です。

アルゴリズム

分析する

ソート辞書

これは .NET に付属しており、内部的にはバイナリ検索ツリーを使用して実装されています (通常のツリーではなく、少なくとも最適化されたツリーである必要があります)。取得時間は O(log n) で、通常の Dictionary (O(1)) よりも少し遅くなります。
挿入と削除はどちらも O(log n) であり、挿入と削除はリアルタイムでソートされます。
ただし、.NET 2.0 のこのクラスには First プロパティがありません。

辞書 + PriorityQueue

辞書は検索がO(1)であることを保証できます。
優先キューは、挿入と削除の両方が O(log n) になることを保証できます。
しかし、優先キューは指定されたアイテムの削除をサポートしていません (少なくとも私が見つけた優先キューはサポートしていません) ので、キャッシュを削除するときにそれを実装する方法がわかりません。

辞書 + バイナリヒープ

バイナリヒープも優先キューであり、分析は上記と同じになるはずです。詳細に評価したわけではありません。

Bツリー

検索、削除、挿入の効率は非常に良く、すべてのデータベースで使用されていますが、実装が複雑で、バグのない B ツリーを作成することはほぼ不可能です。 stl:map はトップダウンの赤黒木であり、検索、削除、挿入はすべて O(log n) であると誰かが言っていましたが、私は C++ を理解しておらず、詳細なテストを行っていません。

辞書 + リスト

検索にはDictが使用されます。
リストはソートに使用されます。
取得、追加、削除には問題ありません。クリア時のみリストをソートする必要があります。このときキャッシュエントリが多いと遅くなる可能性があります。

辞書 + リンクリスト

検索にはDictが使用されます。
LinkedList の追加と削除はどちらも O(1) です。キャッシュを追加する場合は、リンク リストの先頭にノードを追加します。キャッシュを取得する場合は、特定のノード (最初に特定のノードを削除し (O(n))、次に先頭にノードを追加します (O(1))) を先頭に移動します。キャッシュがいっぱいになると、末尾のノードの一部を切り捨てます。

現在、いくつかのソリューションではマルチスレッドでのロックが必要であり、ロックフリーのソリューションを設計するのは簡単ではありません。次のリンクはマルチスレッドをサポートするソリューションですが、原理はまだ完全には理解されていません。

高性能マルチスレッドLRUキャッシュ
http://www.codeproject.com/KB/recipes/LRUCache.aspx

通常のリンクリストを使用したLRUキャッシュのシンプルな実装

以下は、最後のソリューションの簡単な実装です。このソリューションを最適化する価値があるかどうか、または他のどのソリューションがより適しているかについて説明しましょう。

  1. 公共 クラスLRUCacheHelper {
  2. 読み取り専用辞書 _dict;
  3. 読み取り専用リンクリスト _queue =新しいリンクリスト();
  • 読み取り専用オブジェクト _syncRoot =新しいオブジェクト();
  •     プライベート読み取り専用int _max;
  •     パブリックLRUCacheHelper( int容量、 int最大) {
  • _dict =新しい辞書(容量);
  • _max = 最大値;
  • }
  •    
  •     公共  void Add(Kキー、V値) {
  • ロック (_syncRoot) {
  • チェックと切り捨て();
  • _queue.AddFirst(キー); //O(1)  
  • _dict[キー] = 値; //O(1)  
  • }
  • }
  •    
  •     プライベート  void checkAndTruncate() {
  • ロック (_syncRoot) {
  •              int count = _dict.Count; //O(1)  
  •              (カウント >= _max)の場合{
  •                 削除する必要がある数 = 数 / 10 ;
  •                  ( int i = 0 ; i < needRemoveCount; i++) {
  • _dict.Remove(_queue.Last.Value); //O(1)  
  • _queue.RemoveLast(); //O(1)  
  • }
  • }
  • }
  • }
  •    
  •     公共  void Delete(Kキー) {
  • ロック (_syncRoot) {
  • _dict.Remove(キー); //(1)  
  • _queue.Remove(キー); // O(n)  
  • }
  • }
  •     公開V Get(Kキー) {
  • ロック (_syncRoot) {
  • V ret;
  • _dict.TryGetValue(キー、出力ret); //O(1)  
  • _queue.Remove(キー); //O(n)  
  • _queue.AddFirst(キー); //(1)  
  •              retを返します
  • }
  • }
  • }
  • ふと、普通のリンクリストの代わりに両端リンクリストを使えることを思い出しました。リンクリストを両端リンクリストに置き換えて、リンクリストのノードを辞書に保存します。Getメソッドを使用すると、移動するノードを辞書から直接取得し、このノードの前のノードのNextポインタを次のノードに、次のノードのPreviousポインタを前のノードにポイントできます。これにより、ノードを移動する操作がO(1)に簡素化され、キャッシュの読み取り効率が向上します。

    _dict.TryGetValue(キー、出力ret); //O(1)
    ret.Next.Previous = ret.Previous //O(1)
    ret. 前へ. 次へ. = ret. 次へ //O(1)
    _queue.AddFirst(キー); //O(1)

    改良したリンクリストは要件をほぼ満たしています。

    操作する

    基本操作

    複雑

    読む

    辞書取得

    キュー移動

    お 1

    お 1

    消去

    辞書削除

    キュー.削除

    お 1

    お1

    増加

    辞書追加

    キュー.最初に追加

    お 1

    お 1

    切り捨て

    辞書削除

    キュー.RemoveLast

    わかりました

    わかりました

    Kは切り捨てられたキャッシュ要素の数を表す

    切り捨てる場合、キャッシュがいっぱいになったときに、最も使用頻度の低いキャッシュ項目の何パーセントを切り捨てるかを指定できます。

    もう 1 つは、複数のスレッドを使用するときにロックを最適化する方法を確認することです。辞書のスレッドセーフ バージョンがあります。.NET 3.0 の読み取り/書き込みロックを削除し、通常の汎用辞書を ThreadSafelyDictionay にするだけです。パフォーマンスは良好になるはずです。

    リンクリストでのスレッド同期に読み取り/書き込みロックを使用するのは簡単ではありません。せいぜい、ミューテックスロックを使用できますが、ロックの粒度を考慮する必要があります。Move、AddFirst、およびRemoveLastの場合、操作されるノードは1つまたは2つだけです。これらのノードのみをロックする方法を見つけることは可能ですか?Truncateの場合、多くのノードをバッチで操作する必要があるため、大規模なリンクリストロックが必要ですが、このとき、データベースのテーブルロックや行ロックと同様に、他の操作を停止する方法を考慮する必要があります。

    LRUキャッシュ実装コード

    1. 公共 クラスDoubleLinkedListNode {
    2.     パブリックT 値 { 取得; 設定; }
  •    
  •     パブリックDoubleLinkedListNode次へ { 取得; 設定; }
  •    
  •     パブリックDoubleLinkedListNode事前 { 取得; 設定; }
  •    
  •     パブリックDoubleLinkedListNode(T t) { 値 = t; }
  •    
  •     パブリックDoubleLinkedListNode() { }
  •    
  •     公共  void RemoveSelf() {
  • Prior.Next = 次へ;
  • Next.Prior = 以前のもの;
  • }
  •    
  • }
  • 公共 クラスDoubleLinkedList {
  •     保護されたDoubleLinkedListNode m_ヘッド;
  •     プライベートDoubleLinkedListNode m_テール;
  •    
  •     パブリックダブルリンクリスト() {
  • m_Head =新しいDoubleLinkedListNode ();
  • m_Tail = m_Head;
  • }
  •    
  •     パブリックDoubleLinkedList(T t)
  • これ() {
  • m_Head.Next =新しいDoubleLinkedListNode (ト)
  • m_Tail = m_Head.Next;
  • m_Tail.Prior = m_Head;
  • }
  •    
  •     パブリックDoubleLinkedListNodeしっぽ {
  • 取得 {戻り値m_Tail; }
  • }
  •    
  •     パブリックDoubleLinkedListNode AddHead(T t) {
  • ダブルリンクリストノードinsertNode =新しいDoubleLinkedListNode (ト)
  • ダブルリンクリストノード現在のノード = m_Head;
  • insertNode.Prior = null ;
  • 現在のノードを挿入します。
  • 現在のノードを以前のノードに挿入します。
  • m_Head = 挿入ノード;
  •          insertNodeを返します
  • }
  •     公共  voidテールを削除() {
  • m_Tail = m_Tail.Prior;
  • m_Tail.Next = null ;
  •         戻る;
  • }
  • }
  • 公共 クラスLRUCacheHelper {
  •     クラスDictItem {
  •         パブリックDoubleLinkedListNodeノード { 取得; 設定; }
  •         パブリックV 値 { 取得; 設定; }
  • }
  • 読み取り専用辞書_辞書;
  • 読み取り専用 DoubleLinkedList _queue =新しいDoubleLinkedList ();
  • 読み取り専用オブジェクト _syncRoot =新しいオブジェクト();
  •     プライベート読み取り専用int _max;
  •     パブリックLRUCacheHelper( int容量、 int最大) {
  • _dict =新しい辞書(容量);
  • _max = 最大値;
  • }
  •    
  •     公共  void Add(Kキー、V値) {
  • ロック(これ
  • {
  •    
  • チェックと切り捨て();
  • ダブルリンクリストノードv = _queue.AddHead(キー); //O(1)  
  • _dict[key] = new DictItem() { Node = v, Value = value }; //O(1)  
  • }
  • }
  •    
  •     プライベート  void checkAndTruncate() {
  •          int count = _dict.Count; //O(1)  
  •          (カウント >= _max)の場合{
  •             削除する必要がある数 = 数 / 10 ;
  •              ( int i = 0 ; i < needRemoveCount; i++) {
  • _dict.Remove(_queue.Tail.Value); //O(1)  
  • _queue.RemoveTail(); //O(1)  
  • }
  • }
  • }
  •    
  •     公共  void Delete(Kキー) {
  • ロック (これ) {
  • _dict[キー].Node.RemoveSelf();
  • _dict.Remove(キー); //(1)  
  • }
  • }
  •     公開V Get(Kキー) {
  • ロック (これ) {
  • DictItem ret;
  •              if (_dict.TryGetValue(キー、出力ret)) {
  • ret.Node.RemoveSelf();
  • _queue.AddHead(キー);
  •                  ret.Valueを返します
  • }
  •             戻る デフォルト(V);
  • }
  • }
  • }
  • LRUキャッシュパフォーマンステスト

    両端リンクリストでテストした結果、1 秒あたり800,000 回以上の読み取りと 1 秒あたり200,000 回以上の書き込みが可能となり、パフォーマンスは許容範囲内であると感じました。

    プログラムは 200 万のキャッシュ エントリを初期化し、その後エントリを追加し続けます。500 万のエントリが追加されるたびに 10 分の 1 が切り捨てられ、その後もその数は増加し続けます。

    テスト モデルにおける 1 秒あたりの読み取りと書き込みの比率は 7:3 です。以下は、3 つの時点を連続してキャプチャしたパフォーマンス カウンターのグラフです。
    図1

    図2


    図3


    最大メモリは1Gに達し、CPUも平均90%を超えています。ただし、テストの後半では、最後のスクリーンショットに示すように、スループットが1〜2秒間0になることがよくあります。後で観察すると、1〜2秒の一時停止は第2世代メモリのリサイクルであることがわかります。一時停止がない場合、#gen 2コレクションは1つ増加します。これは、リンクリストが原因であると考えられます。リンクリストのノードの追加と削除は、オブジェクトが頻繁に作成および破棄されるため、GCの負荷が非常に高くなります。

    LRU キャッシュフォローアップの改善

    1. 通常の両端リンク リストの代わりにカーソル リンク リストを使用します。プログラムは、まず固定サイズの配列を割り当て、次に配列インデックスを使用してリンク リストを作成します。これにより、ノードが追加または削除されるたびに GC を実行する必要がなくなります。これは手動のメモリ管理と同等ですが、C# で適切な実装をまだ見つけていません。

    2. リンクリストはマルチスレッド環境での使用には適していないという意見もあります。リンクリストの各操作にはミューテックス ロックが必要で、読み取り/書き込みロックも使用できないためです。現在の実装では、スレッド同期にミューテックス ロックを直接使用しており、スループットは 1 秒あたり 70 万から 80 万です。ロックがボトルネックになっているとは感じていません。改善したい場合は、Dictionary を ThreadSafelyDictionary に置き換えてから、リンクリストにミューテックス ロックを使用できます (当初想定されていた、リンクリスト操作で操作するノードをいくつかロックして同時実行の競合を減らすという考え方は、賢明ではなく、厳密ではありません)。

    3. もう一つの箇所は、ロックを細分化することです。リンクリストは依然としてリンクリストですが、リンクリストの各ノードは HashSet です。HashSet に対する操作が読み取り、書き込み、削除のみで、トラバーサルがない場合、スレッド同期は必要ありません (Set はコレクションなので、必要ないと思います。1 つのスレッドが挿入し、1 つのスレッドが削除し、1 つのスレッドが読み取ります。問題はないはずです。最大でも、読み込んだデータがすぐに削除され、Set 全体の構造が破壊されることはありません)。そして、新しいデータを追加するときは、リンク リストの先頭の Set に挿入します。特定のデータを読み取るときは、そのデータがあるノードの Set からデータを削除し、リンク リストの先頭の Set に新しいデータを挿入します。操作を繰り返すと、リンク リストの最後のノードの Set のデータはすべて古いデータになり、安全に削除できます。もちろん、この削除中はリンク リスト全体をロックする必要があります。各 Set には 20w などの上限が必要ですが、Set を安全にトラバースできないため、現在のサイズを取得できません。そのため、Set データを追加および削除する場合は、Interlocked.Decrement() および Interlocked.Increment() を使用して Count を維持する必要があります。Set がいっぱいになると、リンク リストの先頭に新しい Set ノードが追加されます。

    LRU キャッシュ パフォーマンス テスト スクリプト

    1. クラスプログラム {
    2.     プライベート 静的パフォーマンスカウンタ_addCounter;
    3.     プライベート 静的パフォーマンスカウンタ_getCounter;
    4.    
    5.     静的  void Main(文字列[] 引数) {
    6. セットアップカテゴリ();
    7. _addCounter =新しいPerformanceCounter( "wawasoft.lrucache" "add/sec" false );
    8. _getCounter =新しいPerformanceCounter( "wawasoft.lrucache" "get/sec" false );
    9. _addCounter.RawValue = 0 ;
    10. _getCounter.RawValue = 0 ;
    11.    
    12. ランダム rnd = new Random();
    13.         定数 整数最大値 = 500 * 10000 ;
    14.    
    15.    
    16. LRUCacheHelper< int , int > キャッシュ = new LRUCacheHelper< int , int >( 200 * 10000 , max);
    17.    
    18.          ( int i = 10000 * 100000 - 1 ; i >= 0 ; i--)の場合
    19. {
    20.              (i % 10 > 7 )の場合
    21. {
    22. スレッドプール.QueueUserWorkItem(
    23. 代表者
    24. {
    25. キャッシュに追加(rnd.Next( 0 , 10000 ), 0 );
    26. _addCounter.Increment();
    27. });
    28. }
    29.             それ以外 
    30. {
    31. スレッドプール.QueueUserWorkItem(
    32. 代表者
    33. {
    34.                         int pop = キャッシュ.Get(i);
    35. _getCounter.Increment();
    36. });
    37. }
    38. }
    39. コンソールのキーを読み取ります。
    40. }
    41.    
    42.     プライベート 静的  voidセットアップカテゴリ() {
    43.          if (!PerformanceCounterCategory.Exists( "wawasoft.lrucache" )) {
    44.    
    45. カウンター作成データコレクション CCDC =新しいカウンター作成データコレクション();
    46.    
    47. カウンター作成データ addcounter =新しいカウンター作成データ();
    48. カウンタを追加します。カウンタタイプ = PerformanceCounterType.RateOfCountsPerSecond32;
    49. addcounter.CounterName = "追加/秒" ;
    50. CCDC.Add(カウンタを追加します);
    51.    
    52.    
    53. カウンター作成データ getcounter = new CounterCreationData();
    54. getcounter.CounterType = PerformanceCounterType.RateOfCountsPerSecond32;
    55. getcounter.CounterName = "get/sec" ;
    56. CCDC.Add(カウンタを取得);
    57.    
    58. パフォーマンス カウンタ カテゴリを作成します ( "wawasoft.lrucache" "lrucache" 、CCDC)。
    59.    
    60. }
    61. }
    62.    
    63. }

    <<:  マイクロソフトとヤフーが検索広告契約を締結、Bingがヤフーの独占アルゴリズムに

    >>:  A*アルゴリズムのC#実装に関する簡単な説明

    ブログ    
    ブログ    

    推薦する

    LoRAShear: LLM プルーニングと知識回復に関する Microsoft の最新研究

    LoRAShear は、言語モデリング (LLM) を最適化し、知識を保存するために Microso...

    機械学習に基づく自動ネットワークトラフィック分析

    1. 概要現在、機械学習はネットワーク トラフィック分析タスクで広く使用されています。特徴抽出、モデ...

    ...

    オンラインレビューの 7 分の 1 は偽物です。人工知能は役に立つでしょうか?

    目視で観察すると、コメント欄は中国文学の巨匠の密度が比較的高く、侮辱やおどけのレベルも比較的高く、A...

    食品市場における産業用ロボット、2026年までに7億4500万米ドルに達すると予想

    [[433247]]包装食品の需要増加により、食品ロボット市場規模の成長が促進されると予想されます。...

    自動化によって、採用担当者が大規模な適格な人材を特定する方法

    AI ベースの自動化ツールは、候補者データを収集して処理し、候補者の調達、スクリーニング、多様性、そ...

    2018 年に「破壊的な」変化をもたらす 12 のテクノロジー

    [[223288]]人工知能から拡張現実まで、今年、将来を見据えた企業のビジネスを牽引する破壊的なテ...

    調査:消費者の68%がスマート家電がプライベートな会話を盗聴できると考えている

    PCMag が調査を実施したところ、ユーザーの 68% が、さまざまなスマートホーム製品が知らないう...

    ナレッジグラフに加えて、グラフで他に何ができるでしょうか?

    グラフについてはあまり知らないかもしれませんが、ナレッジグラフについて言えば、それは間違いなく現在ホ...

    あなたの声は私のパスです

    最近私の声が盗まれたことで、AI がすでに社会に混乱を引き起こす能力を持っていることが私には明らかに...

    スマートホテルの室内技術トレンドを探る

    オンライン予約プラットフォームは人々の旅行計画の方法に革命をもたらし、モバイルアプリによりユーザーは...

    「検索」は終わり、「レコメンド」も終わるのか?

    ザッカーバーグ氏は最近、苦境に立たされている。 Facebookが名前を「Meta」に変更して以来、...

    大規模モデルの最大のバグは、正解率がほぼゼロであり、GPTからLlamaまで誰も免れないことです。

    GPT-3とLlamaに「AはBである」という単純な知識を教え、​​次にBが何であるかを尋ねました...

    ...