web-dev-qa-db-ja.com

ベクトルとマップのパフォーマンスの混乱

編集:私は具体的にstd::vector線形検索操作をstd::mapバイナリ検索操作と比較しています。ハーブの主張が関係しているように見えたものです。二分探索を使用すると、パフォーマンスがO(N)からO(log N)に移動することはわかっていますが、Herbの主張をテストすることにはなりません。

BjarneStroustrupとHerbSutterはどちらも最近、リンクリストトラバーサル中のキャッシュミスのコストのために、std::vectorが使用されると予想される状況でstd::listがどれほど素晴らしいかについて話しました。 ( http://channel9.msdn.com/Events/Build/2014/2-661 48分マークを参照)

ハーブはさらに声明を出しましたが、順序ベクトルの操作はstd::mapよりもさらに高速でした(51:30から取得した http://i.imgur.com/zX07TZR.png を参照)上記のchannel9ビデオのマーク)私は理解するのが難しいと感じました。そこで、これを実証するための小さなテストを作成しましたが、これらの結果を再現するのに苦労しました: https://ideone.com/MN7DYK

これはテストコードです:

template <typename C>
void test(std::string name, std::vector<int> shuffledInputValues, C & c)
{
   // fill container 'c' with values from 'shuffledInputValues' then erase them all
   {
      std::cout << "testing " << name << "..." << std::endl;
      timer t;

      for (auto val : shuffledInputValues) insert(c, val);
      for (auto val : shuffledInputValues) remove(c, val);
  }
}

// output:
// testing vector...99.189ms
// testing deque...120.848ms
// testing set...4.077ms

std::vectorのパフォーマンスがstd::setよりも1桁遅いことに注意してください。もちろん、これは私が期待した結果ですが、ハーブが主張しようとしていたという主張については混乱しています。

私は何が間違っているのですか?それとも私はハーブの主張を誤解していますか?

私のテストアプリに関するメモ:

  • 線形演算を使用する必要があります-演習の要点はCPUキャッシュの魔法を示すことです。これらは、HerbとBjarneが演習に課した制約です。
  • トリッキーなループを試しませんでした-ベクトルの反復を解き明かしますが、反復はとにかくパフォーマンスにあまり影響を与えていないと思います
  • Ideoneは大きなセットでタイムアウトするため、ループを10Kアイテムに制限しましたが、サイズを大きくしても結果は変わりません。

編集:ルックアップのパフォーマンスのみを比較する変更された例については、 https://ideone.com/916fVd を参照してください。線形探索は同じパフォーマンスを示します。

25
Cechner

参照しやすいように スライド を見つけました(グラフは表示されませんが、独自のファイル形式が原因である可能性があります)。関連するスライドは、解決されている問題を説明する番号39です。

§N個のランダムな整数を生成し、それらをシーケンスに挿入して、それぞれが番号順に適切な位置に挿入されるようにします。

§シーケンス内のランダムな位置を選択し、そこで要素を削除することにより、要素を1つずつ削除します。

さて、リンクリストがこの問題に適した選択ではないことはかなり明白なはずです。リストは、最初または途中で挿入/削除するためのベクトルよりもはるかに優れていますが、random位置での挿入/削除には適していません線形検索が必要なためです。また、キャッシュ効率が向上するため、ベクトルを使用すると線形検索がはるかに高速になります。

Sutterは、O(log n)検索を取得するため、マップ(または一般にツリー)がこのアルゴリズムの自然な選択であるように思われることを示唆しています。実際、挿入部分のN値が大きい場合、ベクトルを非常に簡単に打ち負かします。

ここにbutがあります。 n番目の要素を削除する必要があります(ランダムnの場合)。これはあなたのコードが不正行為をしていると私が信じるところです。挿入された順序で要素を削除し、入力ベクトルを「ランダム」位置にある要素の値を見つけるためのルックアップテーブルとして効果的に使用して、O(log n)で要素を検索できるようにします。したがって、問題を解決するために、実際には集合とベクトルの組み合わせを使用しています。

std::mapまたはstd::set(Sutterが使用したと思います)に使用されるような通常の二分探索木には、n番目の要素を見つけるための高速アルゴリズムがありません。 これが1つです これは平均してO(log n)であり、最悪の場合はO(n)です。しかし、std::mapstd::set基になるツリー構造へのアクセスを提供しないので、順序どおりのトラバーサル(間違っている場合は修正してください)で立ち往生している場合は、線形検索になります!実際には、マップバージョンがSutterの結果ではベクトル1と競合します。

Log(n)の複雑さについては、 Order statistic tree のような構造が必要ですが、残念ながら標準ライブラリでは提供されていません。示されているようにGNUポリシーベースのSTLMAPがあります ここ

これは、ベクトルvsセットvs ost(適切な測定のためのバイナリ検索を使用したベクトル)用に作成した簡単なテストコードです https://ideone.com/DoqL4H セットははるかに低速ですが、他のツリーベースの構造Sutterの結果と一致しないベクトルよりも高速です。

order statistics tree: 15.958ms
vector binary search: 99.661ms
vector linear search: 282.032ms
set: 2004.04ms

(N = 20000、差は大きい値のostに有利になるだけです)

要するに、私はサッターの元の結果は奇妙に見えるが、わずかに異なる理由であるという同じ結論に達しました。今回は、漸近的な複雑さが増すと、一定の要素が少なくなるように思われます。

問題の説明はランダムな値が重複する可能性を排除していないため、multimap/multisetの代わりにmap/setを使用すると、map/setに有利になりますが、値ドメインの場合、重要性は小さいと思います。 Nよりもはるかに大きいです。また、ベクトルを事前に予約しても、パフォーマンスは大幅に向上しません(N = 20000の場合は約1%)。

8
eerorika