バイナリヒープ を効率的に実装する方法についての情報を探しています。ヒープを効率的に実装することについての素晴らしい記事がどこかにあるはずだと思いますが、私はそれを見つけていません。実際、私はの問題に関するリソースを見つけることができませんでした 効率的 ヒープを配列に格納するなどの基本を超えた実装。以下で説明するものを超えて、高速なバイナリヒープを作成するための手法を探しています。
Microsoft Visual C++およびGCCのstd :: priority_queueよりも高速な、またはstd :: make_heap、std :: Push_heap、およびstd :: pop_heapを使用するC++実装をすでに作成しました。以下は、私が実装ですでにカバーしているテクニックです。私は最後の2つだけを思いついたが、それらが新しいアイデアだとは思えない。
(編集:メモリ最適化に関するセクションを追加)
代わりに、一番上の要素があったヒープに穴を残すことができます。次に、大きい方の子を繰り返し上に移動して、その穴をヒープの下に移動します。これには、通過するレベルごとに1つの比較のみが必要です。このようにして、穴は葉になります。この時点で、右端の一番下の葉を穴の位置に移動し、ヒーププロパティが復元されるまでその値を上に移動できます。移動した値は葉だったので、ツリーのはるか上に移動するとは思われません。したがって、log nの比較よりも少し多く、以前よりも優れていると予想する必要があります。
&(p[i]) == static_cast<char*>(p) + sizeof(T) * i
コンパイラは、p [i]を取得するためにこの計算を実行する必要があります。 sizeof(T)はコンパイル時定数であり、sizeof(T)が2の累乗である場合、乗算をより効率的に実行できます。私の実装は、8パディングバイトを追加してsizeof(T)を24から32に増やすことで高速化されました。キャッシュの効率が低下したということは、おそらくこれが十分に大きなデータセットの利点ではないことを意味します。
&(p[i]) == static_cast<char*>(p) + sizeof(T) * i == static_cast<char*>(p) + j
次に、j値の左子式と右子式はそれぞれ2 * jと2 * j + sizeof(T)になります。親の数式はもう少しトリッキーで、j値をi値に変換して、次のように戻す以外の方法は見つかりませんでした。
parentOnJ(j) = parent(j/sizeof(T))*sizeof(T) == (j/(2*sizeof(T))*sizeof(T)
Sizeof(T)が2の累乗である場合、これは2シフトにコンパイルされます。これは、インデックスiを使用する通常の親よりも1操作多くなります。ただし、ルックアップ時に1つの操作を保存します。したがって、正味の効果は、この方法で親を見つけるのに同じ時間がかかる一方で、左子と右子の検索が速くなることです。
TokenMacGuyとtemplatetypedefの回答は、キャッシュミスを減らすメモリベースの最適化を指摘しています。非常に大きなデータセットまたは使用頻度の低い優先キューの場合、OSによってキューの一部をディスクにスワップアウトできます。その場合、ディスクからのスワップインは非常に遅いため、キャッシュを最適に利用するために多くのオーバーヘッドを追加する価値があります。私のデータはメモリに簡単に収まり、継続的に使用されるため、キューのどの部分もディスクにスワップされない可能性があります。これは、優先度付きキューのほとんどの用途に当てはまると思います。
CPUキャッシュをより有効に活用するように設計された他の優先キューがあります。たとえば、4ヒープではキャッシュミスが少なくなり、余分なオーバーヘッドの量はそれほど多くありません。 LaMarca and Ladner 1996年に、整列された4ヒープに移行することでパフォーマンスが75%向上したと報告しています。ただし、 Hendriks 2010年には次のように報告されています。
データの局所性を改善し、キャッシュミスを減らすために、LaMarcaとLadner [17]によって提案された暗黙的なヒープの改善もテストされました。 4ウェイヒープを実装しました。これは、非常に偏った入力データに対しては2ウェイヒープよりもわずかに優れた一貫性を示していますが、キューサイズが非常に大きい場合のみです。非常に大きなキューサイズは、階層ヒープによってより適切に処理されます。
このトピックに関する興味深い論文/記事では、ヒープの全体的なレイアウトでのキャッシュ/ページングの動作について考察しています。データ構造の実装の他のほとんどの部分よりも、キャッシュミスやページインの支払いに非常にコストがかかるという考えです。このホワイトペーパーでは、これに対処するヒープレイアウトについて説明しています。
@TokenMacGuyの投稿の詳細として、 キャッシュを無視するデータ構造 を調べることをお勧めします。アイデアは、任意のキャッシングシステムに対して、キャッシュミスの数を最小限に抑えるデータ構造を構築することです。それらはトリッキーですが、マルチレイヤーキャッシュシステム(たとえば、レジスタ/ L1/L2/VM)を扱う場合でもうまく機能するため、実際にはあなたの観点からは役立つかもしれません。
実際には 最適なキャッシュ忘却優先キューを詳述した論文 興味深いかもしれません。このデータ構造は、すべてのレベルでキャッシュミスの数を最小限に抑えようとするため、速度の点であらゆる種類の利点があります。
バイナリヒープのwikiページでこのリンクを見逃したのか、それとも価値がないと判断したのかはわかりませんが、どちらの方法でも: http://en.wikipedia.org/wiki/B-heap
最初のポイント:アレイベースの実装に「スペアスポット」があることさえ無駄ではありません。とにかく、多くの操作には一時的な要素が必要です。毎回新しい要素を初期化するのではなく、インデックス[0]に専用の要素があると便利です。