このキャッシュ戦略に使用するデータ構造は何ですか?


11

私は.NET 4.0アプリケーションに取り組んでいます。これは、doubleを返す2つのdoubleに対してかなり高価な計算を実行します。この計算は、数千のアイテムのそれぞれに対して実行されます。これらの計算はTask、スレッドプールスレッドで実行されます。

いくつかの予備テストでは、同じ計算が繰り返し実行されることが示されているため、n個の結果をキャッシュしたいと思います。キャッシュがいっぱいになったら、最近使用したアイテムのうち、最も使用頻度の低いアイテムを破棄したいと思います。(編集:キャッシュがいっぱいになったときので、私は少なくとも、しばしばを実現することは、意味がないと私は1つが最も頻繁に使用されると、すぐに新しい結果が計算され、次回に置き換えられるであろうと、新たに計算されたものと結果を置き換えますキャッシュに追加されました)

これを実装するために、私はDictionary<Input, double>Input2つの入力double値を格納するミニクラスになる)を使用して、入力とキャッシュされた結果を格納することを考えていました。ただし、最後に結果がいつ使用されたかを追跡する必要もあります。このためには、キャッシュがいっぱいになったときに辞書から結果を削除するために必要な情報を格納する2番目のコレクションが必要だと思います。このリストを常にソートしておくと、パフォーマンスに悪影響が出るのではないかと心配しています。

これを行うためのより良い(つまりよりパフォーマンスの高い)方法、または私が知らない一般的なデータ構造さえありますか?ソリューションの最適性を判断するには、どのようなことをプロファイリング/測定する必要がありますか?

回答:


12

LRUエビクションキャッシュ(最長未使用時間エビクション)を使用する場合、使用するデータ構造の適切な組み合わせは次のとおりです。

  • 循環リンクリスト(優先キューとして)
  • 辞書

これが理由です:

  • リンクリストにはO(1)の挿入および削除時間があります
  • リストがいっぱいになり、追加の割り当てを実行する必要がない場合、リストノードを再利用できます。

これは、基本的なアルゴリズムの動作方法です。

データ構造

LinkedList<Node<KeyValuePair<Input,Double>>> list; Dictionary<Input,Node<KeyValuePair<Input,Double>>> dict;

  1. 入力を受信しました
  2. 辞書にキーが含まれている場合
    • ノードに保存されている値を返し、ノードをリストの先頭に移動します
  3. 辞書にキーが含まれていない場合
    • 値を計算する
    • リストの最後のノードに値を保存します
    • 最後に値がない場合は、辞書から前のキーを削除します
    • 最後のノードを最初の位置に移動します。
    • (入力、ノード)キーと値のペアを辞書に保存します。

このアプローチの利点は、辞書値の読み取りと設定がO(1)に近づき、リンクリスト内のノードの挿入と削除がO(1)であるということです。つまり、アルゴリズムは値の読み取りと書き込みのためにO(1)に近づいています。キャッシュに保存し、メモリの割り当てとブロックメモリのコピー操作を回避して、メモリの観点から安定させます。


良い点、これまでの最高のアイデア、私見。今日、これに基づいてキャッシュを実装しました。明日、どのようにパフォーマンスが良くなるかをプロファイルして確認する必要があります。
PersonalNexus

3

これは、平均的なPCで自由に使える処理能力を考えると、1回の計算に多くの労力を費やしているように見えます。また、値の一意のペアごとに計算の最初の呼び出しの費用が発生するため、100,000の一意の値のペアでは、少なくとも時間n * 100,000 がかかります。辞書が大きくなると、辞書の値へのアクセスが遅くなる可能性が高いことを考慮してください。辞書のアクセス速度が、計算の速度に対して妥当な利益を提供するのに十分な補償になることを保証できますか?

とにかく、アルゴリズムを最適化する手段を見つけることをおそらく検討する必要があるように聞こえます。このためには、ボトルネックがどこにあるかを確認し、クラスのインスタンス化、リスト走査、データベースに関連するオーバーヘッドの一部を削減する方法があるかどうかを判断するために、Redgate Antなどのプロファイリングツールが必要です。アクセス、またはそれが何であれ、あなたに多くの時間を費やしています。


1
残念ながら、現時点では計算アルゴリズムを変更することはできません。これは、CPUを集中的に使用する高度な数学を使用するサードパーティライブラリであるためです。後で修正する場合は、提案されているプロファイリングツールを必ずチェックしてください。さらに、計算は非常に頻繁に実行され、場合によっては同一の入力で実行されるため、非常に単純なキャッシング戦略を使用した場合でも、予備プロファイリングには明確な利点があります。
PersonalNexus

0

1つの考えは、キャッシュnの結果だけがなぜですか?nが300,000の場合でも、7.2MBのメモリ(およびテーブル構造用の追加メモリ)のみを使用します。もちろん、3つの64ビットの倍精度を想定しています。メモリ空間が不足する心配がない場合は、単純に複雑な計算ルーチン自体にメモ化を適用できます。


キャッシュは1つだけではなく、分析している「アイテム」ごとに1つあり、これらのアイテムは数十万になります。
PersonalNexus

入力がどの「アイテム」からのものであるかは、どのように重要ですか?副作用はありますか?
jk。

@jk。アイテムが異なると、計算に対する入力も大きく異なります。これは、オーバーラップがほとんどないことを意味するので、それらを単一のキャッシュに保持することは意味がないと思います。さらに、異なるアイテムが異なるスレッドに存在する可能性があるため、共有状態を回避するために、キャッシュを個別に保持したいと思います。
PersonalNexus

@PersonalNexus私はこれを計算に2つ以上のパラメータが関与していることを暗示していますか?それ以外の場合、基本的にf(x、y)=何らかの処理を行います。さらに、共有状態はパフォーマンスを妨げるのではなく、パフォーマンスを向上させると思われます
ピータースミス

@PeterSmith 2つのパラメーターが主な入力です。他にもありますが、ほとんど変わりません。そうした場合、キャッシュ全体を破棄します。「共有状態」とは、アイテムのすべてまたはグループの共有キャッシュを意味します。これは他の方法でロックまたは同期する必要があるため、パフォーマンスが低下します。共有状態のパフォーマンスへの影響の詳細
PersonalNexus

0

2番目のコレクションでのアプローチは問題ありません。最小値をすばやく見つけて削除し、キュー内の優先度を変更(増加)できる優先度キューにする必要があります(後者は難しい部分で、ほとんどの単純なプリオキューの実装ではサポートされていません)。C5ライブラリは、それが呼び出され、このようなコレクションを持っていますIntervalHeap

またはもちろん、のような独自のコレクションを構築することもできます SortedDictionary<int, List<InputCount>>。(データと値InputCountを組み合わせたクラスでなければなりません)InputCount

カウント値を変更するときにそのコレクションを更新するには、要素を削除して再挿入します。


0

Peter Smithの答えで指摘されているように、実装しようとしているパターンはmemoizationと呼ばれます。C#では、副作用のない透過的な方法でメモ化を実装するのはかなり困難です。Oliver SturmのC#での関数型プログラミングの本は解決策を提供します(コードは10章でダウンロードできます)。

F#では、はるかに簡単です。もちろん、他のプログラミング言語の使用を開始することは大きな決断ですが、検討する価値があるかもしれません。特に複雑な計算では、メモ化よりも多くのものをプログラムしやすくする必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.