boost :: flat_mapと、mapおよびunordered_mapと比較した場合のパフォーマンス


103

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

ありがとう!


boost.org/doc/libs/1_70_0/doc/html/boost/container/…は、ランダムな挿入には対数時間がかかると主張していることに注意する必要があります。 )時間。以下の@ v.oddouの回答のグラフから明らかなように、それは嘘です。ランダムな挿入はO(n)であり、そのうちのnはO(n ^ 2)時間かかります。
ドンハッチ

@DonHatch github.com/boostorg/container/issuesでこれを報告してませんか?(それは比較の数のカウントを与えているかもしれませんが、移動の数のカウントを伴わない場合、それは確かに誤解を招くものです)
Marc Glisse

回答:


188

私の会社ではごく最近、さまざまなデータ構造でベンチマークを実行したので、一言言っておく必要があると感じています。何かを正しくベンチマークすることは非常に複雑です。

ベンチマーク

ウェブでは、よく設計されたベンチマークを見つけることはほとんどありません。今日まで、私はジャーナリストの方法で行われたベンチマークのみを見つけました(かなり素早く、カーペットの下で数十の変数を一掃しました)。

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. アロケータ
  2. 含まれるタイプのサイズ
  3. 含まれているタイプのコピー操作、割り当て操作、移動操作、構築操作の実装コスト。
  4. コンテナ内の要素の数(問題のサイズ)
  5. タイプには取るに足らない3.-操作があります
  6. タイプはPODです

コンテナは時々割り当てられるため、ポイント1は重要です。CRTの「新規」またはユーザー定義の操作(プール割り当てやフリーリストなど)を使用して割り当てる場合は非常に重要です...

pt 1に興味がある人は、システムアロケータのパフォーマンスへの影響についてgamedevのミステリースレッドに参加してください

ポイント2は、一部のコンテナ(たとえばA)がデータをコピーする時間を失うためであり、タイプが大きいほどオーバーヘッドが大きくなります。問題は、別のコンテナBと比較すると、Aが小さいタイプではBに勝ち、大きいタイプでは負ける可能性があることです。

ポイント3は、コストに重み係数を乗算することを除いて、ポイント2と同じです。

ポイント4は、ビッグOとキャッシュの問題が混在している問題です。一部の複雑度の低いコンテナは、キャッシュの局所性が良好であるため、mapvs などの少数のタイプの複雑度の低いコンテナを大幅に上回ります。vectormap、メモリを断片化する)。そして、いくつかの交差点で、含まれる全体のサイズがメインメモリに「リーク」し始め、キャッシュミスを引き起こすため、それらは失われます。それに加えて、漸近的な複雑さが感じられ始めます。

ポイント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

挿入

編集:

私の以前の結果にはバグが含まれていました:彼らは実際に順序付けされた挿入をテストしましたが、フラットマップに対して非常に高速な動作を示しました。
これらの結果は興味深いので、後でこのページに残しました。
これは正しいテストです: ランダム挿入100

ランダム挿入10000

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

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

警告:今後、2 std::mapflat_mapのとの両方のテストにはバグあり、実際には順序付けされた挿入をテストします(他のコンテナのランダム挿入に対して)。
予約なしの100要素の混合挿入

順序付けされた挿入はバックプッシュにつながり、非常に高速であることがわかります。ただし、グラフ化されていない私のベンチマークの結果から、これは逆挿入の絶対最適性に近いとは言えません。10k要素では、事前に予約されたベクトルで完全な逆挿入最適性が得られます。これにより、300万サイクルが得られます。ここでは、への順序付けされた挿入について4.8Mを観察しますflat_map(したがって、最適の160%)。

予約なしの10000要素の混合挿入 分析:これはベクトルの「ランダム挿入」であることを覚えておいてください。したがって、大規模な10億サイクルは、挿入ごとにデータを半分(平均)上方に(要素ごとに)シフトしなければならないことに起因します。

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

サイズ= 100

100要素のコンテナ内のランド検索

サイズ= 10000

10000要素のコンテナー内のrand検索

反復

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

100中程度のポッドの反復

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

中程度のポッド10000以上の反復

塩の最終粒

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

宜しくお願いします


1
ああ、その場合は理にかなっています。Vectorの一定の償却時間保証は、最後に挿入する場合にのみ適用されます。ランダムな位置に挿入すると、挿入ごとにO(n)が平均化されます。これは、挿入ポイントの後のすべてを前方に移動する必要があるためです。したがって、ベンチマークでは、Nが小さい場合でも、非常に速く爆発する2次の動作が予想されます。AssocVectorスタイルの実装では、たとえば、挿入ごとにソートするのではなく、ルックアップが必要になるまでソートを延期します。ベンチマークを見ずに言うのは難しい。
Billy ONeal 14

1
@BillyONeal:ああ、同僚とコードを調べたところ、犯人が見つかりました。挿入されたキーが一意であることを確認するためにstd :: setを使用したため、「ランダム」挿入が注文されました。これは明らかな不誠実さですが、random_shuffleを使用して修正しました。今、再構築しており、新しい結果が編集としてすぐに表示されます。そのため、現在の状態のテストでは、「順序付き挿入」が非常に高速であることが証明されています。
v.oddou 2014

3
「Intel has a paper」←そしてここにあります
同型写像

5
多分私は明白な何かを見逃しているかもしれませんが、ランダム検索がflat_map比較と比較して遅い理由がわかりませんstd::map-この結果を説明できる人はいますか?
ボイシー2017年

1
これは、今回のブースト実装の特定のオーバーヘッドとして説明するものでありflat_map、コンテナとしてのの固有の特性ではありません。のでAska::バージョンはより速くよりstd::map検索。最適化の余地があることを証明します。期待されるパフォーマンスは漸近的には同じですが、キャッシュの局所性のおかげで、わずかに向上する可能性があります。大きいサイズのセットでは、収束するはずです。
v.oddou 2017年

6

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

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

要素の数が事前にわかっている(事前に確認できるreserve)場合、または挿入/削除がまれであるがルックアップが頻繁である場合に最適な使用法です。イテレータの無効化は、一部のユースケースでは少し面倒になり、プログラムの正確さの点で互換性がなくなります。


1
false :)上記の測定は、findオペレーションのマップがflat_mapよりも高速であることを示しています。ブーストpplは実装を修正する必要があると思いますが、理論的には正しいです。
NoSenseEtAl 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.