キャッシュヒットにより、メモリの局所性によってパフォーマンスが大幅に向上することは、プログラミングの常識です。私は最近知りましたboost::flat_map
、地図のベクターベースの実装がどれであるを。それはあなたの典型的なものほど人気がないようですmap
/ unordered_map
それで私はパフォーマンスの比較を見つけることができませんでした。それはどのように比較され、そのための最良のユースケースは何ですか?
ありがとう!
キャッシュヒットにより、メモリの局所性によってパフォーマンスが大幅に向上することは、プログラミングの常識です。私は最近知りましたboost::flat_map
、地図のベクターベースの実装がどれであるを。それはあなたの典型的なものほど人気がないようですmap
/ unordered_map
それで私はパフォーマンスの比較を見つけることができませんでした。それはどのように比較され、そのための最良のユースケースは何ですか?
ありがとう!
回答:
私の会社ではごく最近、さまざまなデータ構造でベンチマークを実行したので、一言言っておく必要があると感じています。何かを正しくベンチマークすることは非常に複雑です。
ウェブでは、よく設計されたベンチマークを見つけることはほとんどありません。今日まで、私はジャーナリストの方法で行われたベンチマークのみを見つけました(かなり素早く、カーペットの下で数十の変数を一掃しました)。
1)キャッシュのウォーミングについて考慮する必要がある
ベンチマークを実行しているほとんどの人はタイマーの不一致を恐れているため、何千回も実行して全体の時間を費やしています。すべての操作で同じ数千回を実行するように注意し、それを比較できると考えています。
実際のところ、キャッシュはウォームにならず、操作は1回だけ呼び出されるため、実際にはほとんど意味がありません。したがって、RDTSCを使用してベンチマークを行う必要があります。IntelはRDTSCの使用方法を説明する論文を作成しました(パイプラインをフラッシュするためにcpuid命令を使用し、プログラムの開始時にそれを少なくとも3回呼び出して安定化させます)。
2) RDTSC精度測定
私もこれを行うことをお勧めします:
u64 g_correctionFactor; // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;
static u64 const errormeasure = ~((u64)0);
#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // flush OOO instruction pipeline
return __rdtsc();
}
inline void WarmupRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // warmup cpuid.
__cpuid(a, 0x80000000);
__cpuid(a, 0x80000000);
// measure the measurer overhead with the measurer (crazy he..)
u64 minDiff = LLONG_MAX;
u64 maxDiff = 0; // this is going to help calculate our PRECISION ERROR MARGIN
for (int i = 0; i < 80; ++i)
{
u64 tick1 = GetRDTSC();
u64 tick2 = GetRDTSC();
minDiff = std::min(minDiff, tick2 - tick1); // make many takes, take the smallest that ever come.
maxDiff = std::max(maxDiff, tick2 - tick1);
}
g_correctionFactor = minDiff;
printf("Correction factor %llu clocks\n", g_correctionFactor);
g_accuracy = maxDiff - minDiff;
printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif
これは不一致測定器であり、時々-10 ** 18(64ビットの最初の負の値)を取得することを避けるために、すべての測定値の最小値を取ります。
インラインアセンブリではなく組み込み関数の使用に注意してください。現在、最初のインラインアセンブリがコンパイラーでサポートされることはほとんどありませんが、さらに悪いことに、コンパイラーは内部を静的に分析できないため、インラインアセンブリーの周りに完全な順序付けバリアを作成します。一度。したがって、コンパイラが命令を自由に並べ替えることを妨げないため、ここでは組み込み関数が適しています。
3)パラメータ
最後の問題は、通常、シナリオのバリエーションが少なすぎることをテストすることです。コンテナのパフォーマンスは次の影響を受けます。
コンテナは時々割り当てられるため、ポイント1は重要です。CRTの「新規」またはユーザー定義の操作(プール割り当てやフリーリストなど)を使用して割り当てる場合は非常に重要です...
(pt 1に興味がある人は、システムアロケータのパフォーマンスへの影響についてgamedevのミステリースレッドに参加してください)
ポイント2は、一部のコンテナ(たとえばA)がデータをコピーする時間を失うためであり、タイプが大きいほどオーバーヘッドが大きくなります。問題は、別のコンテナBと比較すると、Aが小さいタイプではBに勝ち、大きいタイプでは負ける可能性があることです。
ポイント3は、コストに重み係数を乗算することを除いて、ポイント2と同じです。
ポイント4は、ビッグOとキャッシュの問題が混在している問題です。一部の複雑度の低いコンテナは、キャッシュの局所性が良好であるため、map
vs などの少数のタイプの複雑度の低いコンテナを大幅に上回ります。vector
map
、メモリを断片化する)。そして、いくつかの交差点で、含まれる全体のサイズがメインメモリに「リーク」し始め、キャッシュミスを引き起こすため、それらは失われます。それに加えて、漸近的な複雑さが感じられ始めます。
ポイント5は、コンパイラーがコンパイル時に空または簡単なものを排除できることです。コンテナはテンプレート化されているため、一部の操作を大幅に最適化できます。したがって、各タイプには独自のパフォーマンスプロファイルがあります。
ポイント6はポイント5と同じですが、PODはコピーの構築が単なるmemcpyであり、一部のコンテナーはこれらのケースに特定の実装を持ち、部分的なテンプレートの特殊化、またはSFINAEを使用してTの特性に従ってアルゴリズムを選択できます。
どうやらフラットマップは、Loki AssocVectorのようなソートされたベクターラッパーですが、C ++ 11に付属するいくつかの補足的な最新化により、移動セマンティクスを利用して単一要素の挿入と削除を高速化しています。
これは注文されたコンテナです。ほとんどの人は通常、注文部分を必要としないため、の存在unordered..
。
多分あなたは必要だと考えましたflat_unorderedmap
か?これは、そのようなもの、google::sparse_map
またはそのようなもの、つまりオープンアドレスハッシュマップです。
オープンアドレスハッシュマップの問題はrehash
、新しい拡張フラットランドにすべてをコピーする必要があるのに対し、標準の順序付けされていないマップはハッシュインデックスを再作成する必要があるだけで、割り当てられたデータはそのままです。もちろん欠点は、メモリが地獄のように断片化されていることです。
オープンアドレスハッシュマップの再ハッシュの基準は、容量がバケットファクターのサイズに負荷係数を掛けたサイズを超える場合です。
典型的な負荷係数は次のとおりです0.8
。したがって、それを気にする必要があります。それを埋める前にハッシュマップのサイズを事前に設定できる場合は、常にサイズを事前に設定してください。intended_filling * (1/0.8) + epsilon
これにより、入力中にすべてを誤って再ハッシュおよび再コピーする必要がないことが保証されます。
閉じたアドレスマップ(std::unordered..
)の利点は、これらのパラメーターを気にする必要がないことです。
しかし、これboost::flat_map
は順序付けられたベクトルです。したがって、それは常にlog(N)の漸近的な複雑さを持ち、これはオープンアドレスハッシュマップ(一定の償却時間)ほど良くありません。あなたもそれを考慮する必要があります。
これは、さまざまなマップ(int
キーおよび__int64
/ somestruct
を値として)とを含むテストstd::vector
です。
テストされた型情報:
typeid=__int64 . sizeof=8 . ispod=yes
typeid=struct MediumTypePod . sizeof=184 . ispod=yes
挿入
編集:
私の以前の結果にはバグが含まれていました:彼らは実際に順序付けされた挿入をテストしましたが、フラットマップに対して非常に高速な動作を示しました。
これらの結果は興味深いので、後でこのページに残しました。
これは正しいテストです:
私は実装を確認しましたが、ここでフラットマップに実装された遅延ソートなどはありません。各挿入はオンザフライでソートされるため、このベンチマークは漸近的な傾向を示しています。
マップ:O(N * log(N))
ハッシュマップ:O(N)
ベクトルとフラットマップ:O(N * N)
警告:今後、2 std::map
つflat_map
のとの両方のテストにはバグがあり、実際には順序付けされた挿入をテストします(他のコンテナのランダム挿入に対して)。
順序付けされた挿入はバックプッシュにつながり、非常に高速であることがわかります。ただし、グラフ化されていない私のベンチマークの結果から、これは逆挿入の絶対最適性に近いとは言えません。10k要素では、事前に予約されたベクトルで完全な逆挿入最適性が得られます。これにより、300万サイクルが得られます。ここでは、への順序付けされた挿入について4.8Mを観察しますflat_map
(したがって、最適の160%)。
分析:これはベクトルの「ランダム挿入」であることを覚えておいてください。したがって、大規模な10億サイクルは、挿入ごとにデータを半分(平均)上方に(要素ごとに)シフトしなければならないことに起因します。
3つの要素のランダム検索(クロックを1に正規化)
サイズ= 100
サイズ= 10000
反復
サイズ100以上(MediumPodタイプのみ)
サイズ10000以上(MediumPodタイプのみ)
塩の最終粒
最後に、「ベンチマーク§3Pt1」(システムアロケータ)に戻りたいと思いました。私が開発したオープンアドレスハッシュマップのパフォーマンスについて行っている最近の実験では、いくつかのstd::unordered_map
ユースケースでWindows 7とWindows 8の間で3000%を超えるパフォーマンスギャップを測定しました(ここで説明)。
上記の結果(Win7で作成されたもの)について読者に警告したいと思います。マイレージは異なる場合があります。
宜しくお願いします
flat_map
比較と比較して遅い理由がわかりませんstd::map
-この結果を説明できる人はいますか?
flat_map
、コンテナとしてのの固有の特性ではありません。のでAska::
バージョンはより速くよりstd::map
検索。最適化の余地があることを証明します。期待されるパフォーマンスは漸近的には同じですが、キャッシュの局所性のおかげで、わずかに向上する可能性があります。大きいサイズのセットでは、収束するはずです。
ドキュメントから、これはLoki::AssocVector
私がかなりヘビーユーザーであるものに類似しているようです。これはベクトルに基づいているため、ベクトルの特性を備えています。つまり、
size
を超えると無効になりますcapacity
。capacity
と、オブジェクトを再割り当てして移動する必要があります。つまり、挿入は、end
いつ挿入するという特別な場合を除いて、一定の時間は保証されませんcapacity > size
std::map
、std::map
他と同じパフォーマンス特性を持つバイナリ検索であるキャッシュの局所性によるものよりも高速です。要素の数が事前にわかっている(事前に確認できるreserve
)場合、または挿入/削除がまれであるがルックアップが頻繁である場合に最適な使用法です。イテレータの無効化は、一部のユースケースでは少し面倒になり、プログラムの正確さの点で互換性がなくなります。