簡単なキーの場合にunordered_mapよりもmapを使用する利点はありますか?


371

unordered_mapC ++での最近の話で、ルックアップの効率性(償却済みのO(1)O(log n))のため、以前に使用unordered_mapしたほとんどの場合に使用する必要があることに気付きました。ほとんどの場合、マップを使用します。または、キータイプとしてまたはを使用します。したがって、ハッシュ関数の定義に問題はありません。考えれば考えるほど、単純なタイプのキーの場合にオーバーを使用する理由が見つからないことに気付きました。インターフェースを調べたところ、何も見つかりませんでした。私のコードに影響を与える重要な違い。mapintstd::stringstd::mapstd::unordered_map

そこで質問:使用する任意の本当の理由があるstd::map以上std::unordered_mapのような単純なタイプの場合intとはstd::string

私は厳密にプログラミングの観点から質問しています。これは完全に標準とは見なされておらず、移植に問題が発生する可能性があることを知っています。

また、正しい答えの1つは、オーバーヘッドが小さいため、「データセットが小さいほど効率的である」と思われます(それは本当ですか)。したがって、質問の量がキーは自明ではありません(> 1 024)。

編集: ああ、私は明白なことを忘れました(GManに感謝します!)-はい、もちろん地図は注文されています-私はそれを知っており、他の理由を探しています。


22
私はインタビューでこの質問をするのが好きです:「バブルソートよりクイックソートのほうがいいのはいつですか?」質問への答えは、複雑性理論の実際の応用への洞察を提供し、O(1)などの単純な白黒のステートメントはO(n)より優れているか、O(k)はO(logn)などと同等です。 ..

42
@ベー、私はあなたが「クイックソートよりバブルソートの方がいい時」を意味したと思います:P
コルネル・キジエレビッチ

2
スマートポインターは簡単なキーでしょうか?
thomthom 2013

ここでマップは有利一つれる場合のいずれかである:stackoverflow.com/questions/51964419/...
anilbey

回答:


399

map要素の順序を維持することを忘れないでください。あなたがそれをあきらめることができないなら、明らかにあなたは使うことができませんunordered_map

心に留めておくべき他のことは、unordered_map一般により多くのメモリを使用することです。mapいくつかのハウスキーピングポインタと各オブジェクトのメモリがあります。反対にunordered_map、大きな配列(これらは一部の実装では非常に大きくなる可能性があります)を持ち、次に各オブジェクトに追加のメモリがあります。メモリを意識する必要がある場合mapは、大規模な配列が不足しているため、より良いことがわかるはずです。

だから、もしあなたが純粋な検索検索を必要とするなら、私はunordered_map行く方法だと思います。しかし、常にトレードオフがあり、それらを購入する余裕がない場合、それを使用することはできません。

個人的な経験から、メインエンティティルックアップテーブルのunordered_map代わりに使用すると、パフォーマンス(もちろん測定値)が大幅に改善されたことがわかりましたmap

一方、要素の挿入と削除を繰り返すのは非常に遅いことがわかりました。これは要素の比較的静的なコレクションに最適ですが、大量の挿入と削除を行っている場合、ハッシュとバケットの合計が増えるようです。(これは何度も繰り返したものです。)


3
unordered_mapとmap(またはvectorとlist)のlarge(r)メモリブロックプロパティについてもう1つ、デフォルトのプロセスヒープ(ここではWindowsと通信)がシリアル化されます。マルチスレッドアプリケーションで(小さい)ブロックを大量に割り当てると、非常にコストがかかります。
ROAR

4
RA:特定のプログラムにとって重要であると思われる場合は、独自のアロケーター型を任意のコンテナーと組み合わせることで、多少制御できます。

9
のサイズがわかっていて、unordered_mapそれを最初に予約する場合、それでも多くの挿入のペナルティを支払いますか?たとえば、ルックアップテーブルを作成したときに一度挿入するだけで、後でそれから読み取るだけです。
thomthom 2013

3
@thomthom私が知る限り、パフォーマンスの点でペナルティはないはずです。パフォーマンスがヒットする理由は、配列が大きくなりすぎると、すべての要素の再ハッシュが行われるためです。reserveを呼び出すと、既存の要素が再ハッシュされる可能性がありますが、最初に呼び出す場合は、少なくともcplusplus.com/reference/unordered_map/unordered_map/reserve
Richard Fung

6
記憶的にはそれが逆であると私は確信しています。順序付けられていないコンテナーのデフォルトの1.0負荷係数を想定すると、バケットの要素ごとに1つのポインターがあり、バケット内の次の要素の要素ごとに1つのポインターがあるため、各要素ごとに2つのポインターとデータが得られます。一方、順序付きコンテナの場合、典型的なRBツリーの実装には、3つのポインタ(左/右/親)に加えて、位置合わせのために4番目のワードをとるカラービットがあります。これは、4つのポインタと各要素ごとのデータです。
Yakov Galka 2016

126

std::mapstd::unordered_map実装の速度を比較したい場合は、time_hash_mapプログラムを備えたGoogleのsparsehashプロジェクトを使用して、時間を計ることができます。たとえば、x86_64 Linuxシステム上のgcc 4.4.2

$ ./time_hash_map
TR1 UNORDERED_MAP (4 byte objects, 10000000 iterations):
map_grow              126.1 ns  (27427396 hashes, 40000000 copies)  290.9 MB
map_predict/grow       67.4 ns  (10000000 hashes, 40000000 copies)  232.8 MB
map_replace            22.3 ns  (37427396 hashes, 40000000 copies)
map_fetch              16.3 ns  (37427396 hashes, 40000000 copies)
map_fetch_empty         9.8 ns  (10000000 hashes,        0 copies)
map_remove             49.1 ns  (37427396 hashes, 40000000 copies)
map_toggle             86.1 ns  (20000000 hashes, 40000000 copies)

STANDARD MAP (4 byte objects, 10000000 iterations):
map_grow              225.3 ns  (       0 hashes, 20000000 copies)  462.4 MB
map_predict/grow      225.1 ns  (       0 hashes, 20000000 copies)  462.6 MB
map_replace           151.2 ns  (       0 hashes, 20000000 copies)
map_fetch             156.0 ns  (       0 hashes, 20000000 copies)
map_fetch_empty         1.4 ns  (       0 hashes,        0 copies)
map_remove            141.0 ns  (       0 hashes, 20000000 copies)
map_toggle             67.3 ns  (       0 hashes, 20000000 copies)

2
ほとんどの操作では、順序付けられていないマップがマップに勝っています。挿入
Michael IV

7
sparsehashはもう存在しません。削除または削除されました。
User9102d82 2018年

1
@ User9102d82 waybackmachineリンクを参照するように質問を編集しました。
andreee

他の人が時間以外にも他の数字に気付くようにするためだけに:これらのテストは、4バイトのオブジェクト/データ構造(別名int)で行われました。より重いハッシュを必要とする、またはより大きなもの(コピー操作をより重くする)を格納する場合、標準マップはすぐに有利になる可能性があります。
AlexGeorg

82

GManが作成したのとほぼ同じ点をエコーし​​ます。使用の種類によっては、(VS 2008 SP1に含まれている実装を使用してstd::map)よりも高速である可能性があります(多くの場合高速ですstd::tr1::unordered_map)。

覚えておくべきいくつかの複雑な要因があります。たとえば、ではstd::map、キーを比較しています。つまり、ツリーの右側と左側のサブブランチを区別するのに十分なだけ、キーの先頭を確認するだけです。私の経験では、キー全体を確認するのは、単一の命令で比較できるintのようなものを使用している場合に限ります。std :: stringのようなより一般的なキータイプでは、数文字程度しか比較しないことがよくあります。

対照的に、まともなハッシュ関数は常にキー全体を調べます。IOW、テーブルルックアップが一定の複雑さであっても、ハッシュ自体はおおよそ線形の複雑さを持ちます(ただし、項目の長さではなく、キーの長さでは)。キーとして長い文字列を使用すると、std::mapは検索を開始する前に検索を終了するunordered_map場合あります。

第2に、ハッシュテーブルのサイズを変更する方法はいくつかありますが、それらのほとんどはかなり低速です- 挿入と削除よりもルックアップの頻度がかなり高い場合を除いて、std :: mapはしばしばより高速ですstd::unordered_map

もちろん、前の質問のコメントで述べたように、木のテーブルを使用することもできます。これには利点と欠点の両方があります。一方で、それは最悪のケースを木のケースに限定します。また、固定サイズのテーブルを使用したため(少なくとも私が実行した場合)、挿入と削除を高速に行うことができます。すべてのテーブルのサイズ変更を排除することで、ハッシュテーブルをずっと単純に、通常は速く保つことができます。

もう1つのポイント:ハッシュとツリーベースのマップの要件は異なります。ハッシュ化には明らかにハッシュ関数と等値比較が必要ですが、順序付けされたマップには「より小」の比較が必要です。もちろん、私が言及したハイブリッドには両方が必要です。もちろん、キーとして文字列を使用する一般的なケースでは、これは実際には問題ありませんが、一部のタイプのキーはハッシュよりも順序付けに適しています(またはその逆)。


2
ハッシュのサイズ変更は、dynamic hashing技術によって緩和できます。これは、アイテムを挿入するたびにk他のアイテムも再ハッシュする移行期間を持つことで構成されます。もちろん、これは移行中に2つの異なるテーブルを検索する必要があることを意味します...
Matthieu M.

2
「長い文字列をキーとして使用すると、std :: mapはunordered_mapが検索を開始する前に検索を終了する可能性があります。」-キーがコレクションに存在しない場合。存在する場合は、もちろん全長を比較して一致を確認する必要があります。しかし、同様にunordered_map完全な比較でハッシュの一致を確認する必要があるため、すべて比較するルックアッププロセスの部分に依存します。
Steve Jessop、2014年

2
通常、データの知識に基づいてハッシュ関数を置き換えることができます。あなたの長い文字列は、最初の100に比べて最後の20バイトにより異なる場合、たとえば、ちょうど最後の20をハッシュ
エリックAronesty

56

私が注文したマップは(からダウンロードすることができますいくつかの実験の後、長い文字列のパフォーマンスの向上を示すであろうことを示唆し@Jerry棺、からの回答に興味をそそられたペーストビン)、私はこれが唯一のコレクションのためにも当てはまるように思われることを発見しましたランダム文字列の場合、ソートされた辞書(かなりの量の接頭辞が重複した単語を含む)でマップが初期化されると、おそらく値を取得するために必要なツリーの深さが増加するため、このルールは機能しなくなります。結果を以下に示します。最初の数値列は挿入時間、2番目はフェッチ時間です。

g++ -g -O3 --std=c++0x   -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
gmurphy@interloper:HashTests$ ./stdtests
# 1st number column is insert time, 2nd is fetch time
 ** Integer Keys ** 
 unordered:      137      15
   ordered:      168      81
 ** Random String Keys ** 
 unordered:       55      50
   ordered:       33      31
 ** Real Words Keys ** 
 unordered:      278      76
   ordered:      516     298

2
テストをありがとう。ノイズを測定していないことを確認するために、各操作を何度も行うように変更しました(マップに1の代わりにカウンターを挿入しました)。私はそれを異なる数のキー(2から1000)で実行し、マップ内で最大100個までのキーを実行しました。std::map通常std::unordered_map、特に整数キーの場合はパフォーマンスが優れていますが、100個以下のキーはエッジを失い、std::unordered_map勝ち始めます。すでに順序付けられたシーケンスをaに挿入することstd::mapは非常に悪いことです。最悪のシナリオ(O(N))になります。
Andreas Magnusson

30

ただ指摘しておきますが、unordered_mapS にはさまざまな種類があります。

ハッシュマップでウィキペディアの記事を検索してください。使用された実装に応じて、ルックアップ、挿入、および削除に関する特性はかなり大きく異なる場合があります。

そして、それunordered_mapがSTLへの追加で私が最も心配していることです。彼らはPolicy道を行くと思うので、彼らは特定の実装を選択する必要があります。したがって、平均的な使用のための実装で立ち往生します。他の場合...

たとえば、一部のハッシュマップには線形の再ハッシュがあり、ハッシュマップ全体を一度に再ハッシュする代わりに、挿入ごとに一部が再ハッシュされるため、コストの償却に役立ちます。

別の例:一部のハッシュマップはバケットのノードの単純なリストを使用し、他はマップを使用し、他はノードを使用せずに最も近いスロットを見つけ、最後にいくつかはノードのリストを使用しますが、最後にアクセスされた要素になるように並べ替えます前面にあります(キャッシングのように)。

そのため、現時点では(凍結されたデータセットに対して)std::mapまたはloki::AssocVector(または)を好む傾向があります。

誤解しないでください。std::unordered_map将来的には使用したいのですが、そのようなコンテナの実装方法と、その結果として生じるさまざまなパフォーマンスを考えると、このようなコンテナの移植性を「信頼する」ことは困難です。これの。


17
+1:有効な点-自分の実装を使用していたときの方が楽だった-少なくともどこが
悪いの

25

ここでは十分に言及されていない重要な違い:

  • mapすべての要素に対するイテレータを安定させます。C++ 17では、mapイテレータを無効にすることなく要素間で要素を移動することもできます(潜在的な割り当てなしに適切に実装されている場合)。
  • map 単一の操作のタイミングは、大規模な割り当てを必要としないため、通常はより一貫しています。
  • unordered_map使用したstd::hash播種は本当に助け、見ることがないことを-信頼できない入力を与えた場合のlibstdc ++で実装されていることはDoS攻撃に対して脆弱である(それは一定の種子でMurmurHash2を使用していますhttps://emboss.github.io/blog/2012/12/14/破壊-雑音-ハッシュ-洪水-ド-リロード/)。
  • 順序付けられていると、効率的な範囲検索が可能になります。たとえば、キーが42以上のすべての要素を反復処理します。

14

ハッシュテーブルは、一般的なマップの実装よりも定数が高く、小さなコンテナでは重要になります。最大サイズは10、100、あるいは1,000以上ですか?定数はこれまでと同じですが、O(log n)はO(k)に近いです。(対数の複雑さは依然として非常に優れています。)

優れたハッシュ関数を作成する方法は、データの特性によって異なります。したがって、カスタムハッシュ関数を検討する予定がない場合(ただし、間違いなく後で簡単に変更できるため、ほぼすべてをtypedefするため、デフォルトが多くのデータソースに対して適切に実行されるように選択されている場合でも、順序付けされていることがわかります)マップの性質上、最初は十分な助けになるので、その場合はハッシュテーブルではなくマップをデフォルトに設定します。

それに加えて、他の(通常はUDT)型のハッシュ関数を書くことを考える必要はなく、単にop <(とにかく欲しい)を書くだけです。


@ロジャー、あなたはunordered_mapがマップする要素のおおよその量を知っていますか?とにかく、私はおそらくそのためのテストを書くでしょう...(+1)
Kornel Kisielewicz

1
@Kornel:それほど多くはかかりません。私のテストは約10,000要素でした。私たちが望む場合は、本当に正確なグラフを、あなたはの実装を見ることができるmapとの1 unordered_map特定のプラットフォームおよび特定のキャッシュサイズで、かつ複雑な分析を行います。:P
GManNickG 2010

実装の詳細、コンパイル時のチューニングパラメータ(独自の実装を作成している場合はサポートが容易)、およびテストに使用される特定のマシンによって異なります。他のコンテナと同様に、委員会は広範な要件のみを設定します。

13

理由は他の答えで示されています。ここに別のものがあります。

std :: map(バランスバイナリツリー)操作は、償却O(log n)と最悪の場合O(log n)です。std :: unordered_map(ハッシュテーブル)操作は、償却O(1)と最悪の場合O(n)です。

これが実際にどのように機能するかは、ハッシュテーブルがO(n)操作で時々「しゃっくり」するということです。これは、アプリケーションが許容できる場合とそうでない場合があります。許容できない場合は、std :: unordered_mapよりもstd :: mapを使用することをお勧めします。


12

概要

順序付けは重要ではないと仮定します。

  • 大きなテーブルを一度作成して、多数のクエリを実行する場合は、 std::unordered_map
  • 小さなテーブル(100要素未満になる可能性があります)を作成して多数のクエリを実行する場合は、を使用しますstd::map。これは、それを読み取るためO(log n)です。
  • あなたがテーブルを頻繁に変更するつもりなら、それは良いオプションかもしれません std::map
  • 疑問がある場合は、を使用してくださいstd::unordered_map

歴史的背景

ほとんどの言語では、順序付けられていないマップ(別名ハッシュベースの辞書)がデフォルトのマップですが、C ++では順序付けられたマップがデフォルトのマップとして取得されます。どうしてこうなりました?一部の人々は、C ++委員会が独自の知恵でこの決定を下したと誤って想定していますが、真実は残念ながらそれよりも醜いです。

C ++では、実装方法に関するパラメーターが多すぎないため、C ++はデフォルトで順序付きマップになっていたと広く信じられています。一方、ハッシュベースの実装には、話し合うべきことがたくさんあります。したがって、標準化におけるグリッドロックを回避するため、順序付けられたマップとうまくやり取りしました。2005年頃には、多くの言語でハッシュベースの実装が既に適切に実装されていたため、委員会が新しいを受け入れる方が簡単でしたstd::unordered_map。完全な世界でstd::mapは、無秩序でありstd::ordered_map、別のタイプとして持っているでしょう。

パフォーマンス

以下の2つのグラフは、それ自体がわかるはずです(ソース)。

ここに画像の説明を入力してください

ここに画像の説明を入力してください


興味深いデータ。テストにいくつのプラットフォームを含めましたか?
Toby Speight 2018年

1
ここに投稿した2つの画像によると、std :: unordered_mapは常にstd :: mapよりもパフォーマンスが高いため、多くのクエリを実行するときに小さなテーブルにstd :: mapを使用する必要があるのはなぜですか?
リッキー

グラフは、0.13M以上の要素のパフォーマンスを示しています。要素が小さい(100未満の場合もある)場合、O(log n)は順序付けられていないマップよりも小さくなる可能性があります。
Shital Shah、2018

10

私は最近50000のマージとソートを行うテストを行いました。つまり、文字列キーが同じ場合は、バイト文字列をマージします。そして、最終的な出力をソートする必要があります。したがって、これにはすべての挿入のルックアップが含まれます。

以下のためmapの実装、それは仕事を終えるために200ミリ秒かかります。unordered_map+の場合、挿入にmapは70ミリ秒、unordered_map挿入には80ミリ秒かかりますmap。したがって、ハイブリッド実装は50ミリ秒速くなります。

を使用する前に、よく考える必要がありますmap。プログラムの最終結果でデータを並べ替えるだけでよい場合は、ハイブリッドソリューションの方が適している場合があります。


0

上記すべてに小さな追加:

map要素が並べ替えられ、ある境界から別の境界まで反復することができるため、範囲で要素を取得する必要がある場合は、を使用することをお勧めします。


-1

差出人:http : //www.cplusplus.com/reference/map/map/

「内部的に、マップ内の要素は常に、その内部比較オブジェクト(タイプCompare)によって示される特定の厳密な弱い順序付け基準に従って、キーによってソートされます。

通常、マップコンテナーはunordered_mapコンテナーよりも個々の要素にキーでアクセスするのに時間がかかりますが、順序に基づいてサブセットを直接反復できます。」

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.