プログラミングでは、メモリの局所性により、キャッシュヒットによりパフォーマンスが大幅に向上することがよく知られています。最近、マップのベクターベースの実装であるboost::flat_map
について知りました。一般的なmap
/unordered_map
ほど人気がないようですので、パフォーマンスの比較を見つけることができませんでした。それをどのように比較し、その最適なユースケースは何ですか?
ありがとう!
会社でさまざまなデータ構造のベンチマークを実行したのはごく最近のことなので、Wordを削除する必要があると感じています。何かを正しくベンチマークすることは非常に複雑です。
ウェブ上で、うまく設計されたベンチマークを見つけることはほとんどありません。今日まで、私はジャーナリストのやり方で行われたベンチマークを見つけただけでした(非常に素早く、カーペットの下にある何十もの変数を一掃しました)。
1)キャッシュウォーミングについて考慮する必要があります
ベンチマークを実行しているほとんどの人はタイマーの不一致を恐れているため、何千回も実行し、すべての時間を費やします。すべての操作で同じ千回を実行するように注意し、それを比較します。
真実は、実世界ではほとんど意味がありません。なぜなら、キャッシュは暖かくならず、操作はたった一度だけ呼び出されるからです。そのため、RDTSCを使用してベンチマークを実行し、それらを一度だけ呼び出すように時間をかける必要があります。 Intelは論文を作成しました description 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 = Aska::Min(minDiff, tick2 - tick1); // make many takes, take the smallest that ever come.
maxDiff = Aska::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ビットの最初の負の値)が時々取得されるのを避けるために、すべての測定値の最小値を取ります。
インラインアセンブリではなく、組み込み関数の使用に注意してください。最近のコンパイラでは最初のインラインアセンブリがサポートされることはめったにありませんが、さらに悪いことに、コンパイラは内部を静的に分析できないため、インラインアセンブリの周りに完全な順序付けバリアを作成します。一度。したがって、コンパイラの命令の自由な再順序付けを壊さないため、ここでは組み込み関数が適しています。
)パラメーター
最後の問題は、通常、人々がテストするシナリオのバリエーションが少なすぎることです。コンテナのパフォーマンスは次の影響を受けます。
コンテナは時々割り当てを行うため、ポイント1は重要です。CRTの「新規」またはプール割り当てや空きリストなどのユーザー定義の操作を使用して割り当てる場合、非常に重要です。
(pt 1に興味がある人のために gamedevのミステリースレッドに参加 システムアロケータのパフォーマンスへの影響について)
ポイント2は、一部のコンテナ(たとえばA)が周りをコピーする時間を失い、タイプが大きいほどオーバーヘッドが大きくなるためです。問題は、別のコンテナBと比較すると、Aは小さなタイプではBに勝ち、大きなタイプでは負ける可能性があることです。
ポイント3は、コストに何らかの重み係数を乗算することを除いて、ポイント2と同じです。
ポイント4は、大きなOとキャッシュの問題が混在する問題です。いくつかの悪い複雑さのコンテナは、少数の型(map
vs. vector
など、キャッシュの局所性は優れているがmap
はメモリを断片化する)の場合、低複雑さのコンテナを大幅に上回ることがあります。そして、含まれている全体サイズがメインメモリに「リーク」してキャッシュミスを引き起こし、それに加えて漸近的な複雑さが感じられるようになるため、ある交差点で失われます。
ポイント5は、コンパイラーがコンパイル時に空または些細なものを削除できることです。これにより、コンテナがテンプレート化されるため、一部の操作が大幅に最適化されます。したがって、各タイプには独自のパフォーマンスプロファイルがあります。
ポイント6はポイント5と同じですが、PODはコピー構成が単なるmemcpyであるという事実から恩恵を受けることができ、一部のコンテナーは部分テンプレート特殊化、またはTの特性に応じてアルゴリズムを選択するSFINAEを使用して、これらのケースに特定の実装を持つことができます.
どうやらフラットマップは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)
警告:以降、std::map
とflat_map
sの2つのテストはbuggyで、実際にテストしますordered Insertion(vs random他のコンテナへの挿入。はい、わかりにくいです):
順序付けられた挿入が確認でき、逆押しになり、非常に高速です。しかし、ベンチマークのグラフ化されていない結果から、これは逆挿入の絶対的な最適性に近いとは言えません。 1万個の要素では、事前に予約されたベクトルで完全な逆挿入の最適性が得られます。これにより、300万サイクルが得られます。ここでflat_map
への順序付き挿入で480万を観測します(したがって最適の160%)。
分析:これはベクトルの「ランダム挿入」であるため、挿入のたびにデータの半分(平均)を(平均して)1要素ずつ上にシフトする必要があるため、10億サイクルが発生します。
つの要素のランダム検索(クロックを1に再正規化)
サイズ= 100
サイズ= 10000
反復
サイズ100以上(MediumPodタイプのみ)
オーバーサイズ10000(MediumPodタイプのみ)
塩の最終粒子
最後に、「ベンチマーク§3Pt1」(システムアロケーター)に戻りたいと思いました。 私が開発したオープンアドレスハッシュマップ のパフォーマンスについて行っている最近の実験では、いくつかのstd::unordered_map
使用でWindows 7とWindows 8の間のパフォーマンスギャップを3000%以上測定しました。ケース( ここで説明 )。
上記の結果について読者に警告したいのですが(Win7で作成されました):走行距離は異なる場合があります。
宜しくお願いします
ドキュメントから、これは私がかなりヘビーユーザーであるLoki::AssocVector
に類似しているようです。それはベクトルに基づいているため、ベクトルの特性を持っています、つまり:
size
がcapacity
を超えると無効になります。capacity
を超えて大きくなると、オブジェクトを再割り当てして移動する必要があります。つまり、capacity > size
のときにend
に挿入する特別な場合を除いて、挿入は一定時間保証されません。std::map
と同じパフォーマンス特性を持つバイナリ検索により、ルックアップはstd::map
より高速です最適な使用方法は、要素の数を事前に知っている場合(したがってreserve
upfrontにできる場合)、または挿入/削除はまれだが検索が頻繁に行われる場合です。イテレータの無効化により、一部のユースケースでは少し面倒になるため、プログラムの正確性の点で交換できません。