web-dev-qa-db-ja.com

boost :: flat_mapおよびそのパフォーマンスとmapおよびunordered_mapとの比較

プログラミングでは、メモリの局所性により、キャッシュヒットによりパフォーマンスが大幅に向上することがよく知られています。最近、マップのベクターベースの実装であるboost::flat_mapについて知りました。一般的なmap/unordered_mapほど人気が​​ないようですので、パフォーマンスの比較を見つけることができませんでした。それをどのように比較し、その最適なユースケースは何ですか?

ありがとう!

96
naumcho

会社でさまざまなデータ構造のベンチマークを実行したのはごく最近のことなので、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. アロケーター
  2. 含まれるタイプのサイズ
  3. 含まれるタイプのコピー操作、割り当て操作、移動操作、構築操作の実装コスト。
  4. コンテナ内の要素の数(問題のサイズ)
  5. タイプには些細な3.操作があります
  6. タイプはPODです

コンテナは時々割り当てを行うため、ポイント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

挿入

編集:

私の以前の結果にはバグが含まれていました。彼らは実際に順序付き挿入をテストしました。
これらの結果は興味深いので、このページの後半に残しました。
これは正しいテストです: enter image description here

enter image description here

実装を確認しましたが、ここではフラットマップに実装された遅延並べ替えなどはありません。各挿入はその場でソートされるため、このベンチマークは漸近的な傾向を示します。

マップ:O(N * log(N))
ハッシュマップ:O(N)
ベクトルとフラットマップ:O(N * N)

警告:以降、std::mapflat_mapsの2つのテストはbuggyで、実際にテストしますordered Insertion(vs random他のコンテナへの挿入。はい、わかりにくいです):
random insert of 100 elements without reservation

順序付けられた挿入が確認でき、逆押しになり、非常に高速です。しかし、ベンチマークのグラフ化されていない結果から、これは逆挿入の絶対的な最適性に近いとは言えません。 1万個の要素では、事前に予約されたベクトルで完全な逆挿入の最適性が得られます。これにより、300万サイクルが得られます。ここでflat_mapへの順序付き挿入で480万を観測します(したがって最適の160%)。

random insert of 10000 elements without reservation 分析:これはベクトルの「ランダム挿入」であるため、挿入のたびにデータの半分(平均)を(平均して)1要素ずつ上にシフトする必要があるため、10億サイクルが発生します。

つの要素のランダム検索(クロックを1に再正規化)

サイズ= 100

Rand search within container of 100 elements

サイズ= 10000

Rand search within container of 10000 elements

反復

サイズ100以上(MediumPodタイプのみ)

Iteration over 100 medium pods

オーバーサイズ10000(MediumPodタイプのみ)

Iteration over 10000 medium pods

塩の最終粒子

最後に、「ベンチマーク§3Pt1」(システムアロケーター)に戻りたいと思いました。 私が開発したオープンアドレスハッシュマップ のパフォーマンスについて行っている最近の実験では、いくつかのstd::unordered_map使用でWindows 7とWindows 8の間のパフォーマンスギャップを3000%以上測定しました。ケース( ここで説明 )。
上記の結果について読者に警告したいのですが(Win7で作成されました):走行距離は異なる場合があります。

宜しくお願いします

175
v.oddou

ドキュメントから、これは私がかなりヘビーユーザーであるLoki::AssocVectorに類似しているようです。それはベクトルに基づいているため、ベクトルの特性を持っています、つまり:

  • イテレータは、sizecapacityを超えると無効になります。
  • capacityを超えて大きくなると、オブジェクトを再割り当てして移動する必要があります。つまり、capacity > sizeのときにendに挿入する特別な場合を除いて、挿入は一定時間保証されません。
  • キャッシュの局所性、つまりstd::mapと同じパフォーマンス特性を持つバイナリ検索により、ルックアップはstd::mapより高速です
  • リンクされたバイナリツリーではないため、メモリの使用量が少ない
  • 強制的に指示しない限り、縮小することはありません(再割り当てをトリガーするため)

最適な使用方法は、要素の数を事前に知っている場合(したがってreserve upfrontにできる場合)、または挿入/削除はまれだが検索が頻繁に行われる場合です。イテレータの無効化により、一部のユースケースでは少し面倒になるため、プログラムの正確性の点で交換できません。

6
Ylisar