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#実装に関する簡単な説明

    ブログ    

    推薦する

    自動運転ブームがAIチップ戦争に火をつけ、爆発したのはテスラだけではない

    以前から大きく騒がれ、メディアもその信憑性を証明する手がかりを繰り返し探していた「テスラの自社開発A...

    ICLRは深層生成モデルに関する大きな議論を開催し、ウェリングとAAAIの百万ドル賞受賞者が来場する。

    この記事はAI新メディアQuantum Bit(公開アカウントID:QbitAI)より許可を得て転載...

    AIを活用して衛星画像を判別、世界初「全世界の船舶足跡マップ」を公開

    1月4日、研究者のデイビッド・クルーズマ氏はナショナルジオグラフィックとブルームバーグ・フィランソロ...

    ...

    ...

    仕事の未来に向けたスマートデバイスの準備

    パンデミック以前は、スマートデバイスは接続できなかった可能性があります。しかし、従業員が自宅からログ...

    ...

    人工知能のコスト問題をどう解決するか?顔認識によって情報セキュリティはどのように確保されるのでしょうか?

    [[422539]] 9月7日午後、第19回「海南省科学技術会議」に新たに追加されたホットトピック...

    クラウドで必要な 5 つの機械学習スキル

    機械学習と AI は IT サービスにさらに深く浸透し、ソフトウェア エンジニアが開発したアプリケー...

    機械学習アルゴリズムは簡単に詐欺を検出できるので、詐欺を恐れる必要はありません。

    実のところ、誰もが詐欺防止を必要としているわけではありません。金融機関が最新の犯罪手法に追いつこうと...

    IT運用保守プラットフォームアルゴリズムの背後にある2つの「神の助け」

    [51CTO.comからの原文] インテリジェント運用保守(AIops)は、IT運用保守の分野で最...

    ...

    将来、人工知能技術は動物実験に取って代わる可能性を秘めているのでしょうか?

    動物実験は動物に対して行われる最も残酷な行為の一つと考えられています。研究によると、マウス、カエル、...

    ...

    アルゴリズム設計者が新たな人気者になる

    Aisle50 の共同創設者であるクリストファー・シュタイナー氏は、新著の中で、デジタルが優位性を...