web-dev-qa-db-ja.com

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

C++でのunordered_mapに関する最近の話で、私は以前はmapを使っていたほとんどの場合、unordered_mapを使うべきだと気付きました。検索の効率のため、(償却O(1)vs.O(log n))ほとんどの場合、私はマップとしてintまたはstd::stringsをキーとして使用します。したがって、ハッシュ関数の定義に問題はありません。 std::mapより単純な型の場合にunordered_mapを使用する理由を見つけることができないということに気づくほど、私は気付くようになりました - 私はインターフェースを調べました、そして重要なことは何も見つけませんでした私のコードに影響を与えるだろう違い。

それ故、質問です - intstd::mapのような単純型の場合にunordered mapよりstd::stringを使う本当の理由は何ですか?

私は厳密にプログラミングの観点から質問しています - 私はそれが完全に標準と見なされていないこと、そしてそれが移植の問題を引き起こすかもしれないことを知っています。

また、正しい答えの1つは、「データセットが小さいほど効率的」になると予想されます(オーバーヘッドが小さいためです)。 - それ故、私は質問をキーの数が自明でない(> 1 024)場合に限定したい。

編集:ああ、当たり前のこと忘れてました(ありがとうGMan!) - はい、地図はもちろん注文されています - 他にも理由があるので探しています

330

mapを忘れずに、要素を順番に並べてください。あなたがそれをあきらめることができないなら、明らかにあなたはunordered_mapを使うことができません。

他に念頭に置いておくべきことはunordered_mapは一般により多くのメモリを使用するということです。 mapは、いくつかのハウスキーピングポインタを持ち、それから各オブジェクトのメモリを持ちます。反対に、unordered_mapは大きな配列(いくつかの実装ではかなり大きくなる可能性があります)を持ち、それから各オブジェクトに追加のメモリを持ちます。あなたがメモリを意識する必要があるなら、mapは大きな配列を欠いているので、より良く証明されるべきです。

それで、もしあなたが純粋な検索 - 検索を必要とするなら、私はunordered_mapが行くべき道であると言うでしょう。しかし、常にトレードオフがあり、それらを買う余裕がない場合は使用できません。

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

一方で、要素の挿入と削除を繰り返すのははるかに遅いことがわかりました。比較的静的な要素の集まりには最適ですが、大量の挿入や削除を行っている場合は、ハッシュ+バケット化が足りないようです。 (注:これは何度も繰り返しました。)

363
GManNickG

あなたのstd::mapstd::unordered_mapの実装のスピードを比較したいのなら、time_hash_mapプログラムで時間を計るグーグルの 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)
111
Blair Zajac

私は、GManが述べたのとほぼ同じ要点を述べています。使用の種類によっては、std::mapstd::tr1::unordered_mapよりも速くなることがあります(VS 2008 SP1に含まれる実装を使用)。

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

対照的に、まともなハッシュ関数は、常にentireキーを調べます。つまり、テーブルルックアップが一定の複雑さであっても、ハッシュ自体はほぼ線形の複雑さを持ちます(ただし、項目の数ではなくキーの長さに依存します)。キーとして長い文字列を使うと、std::mapstartその検索の前にunordered_mapが検索を終了するかもしれません。

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

もちろん、前の質問に対するコメントで述べたように、木の表を使うこともできます。これには長所と短所の両方があります。一方では、最悪の場合を木の場合に限定します。 (少なくともそれをしたときには)固定サイズのテーブルを使用したので、高速な挿入と削除も可能です。allテーブルのサイズ変更を排除すると、ハッシュテーブルをずっとシンプルにして、通常は速くすることができます。

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

76
Jerry Coffin

私は@Jerry Coffinからの回答に興味をそそられました、それは順序付けられた地図が長い文字列上でパフォーマンスの向上を示すことを示唆していました(これは Pastebin からダウンロードできます)。ランダムな文字列のコレクションにのみ当てはまるようですが、マップがソートされた辞書(かなりの量のプレフィックス重複を含む単語を含む)で初期化されると、おそらく値を取得するのに必要なツリーの深さが増えるため、この規則は破綻します。結果を以下に示します。1番目の数字列は挿入時間、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
52
Gearoid Murphy

ただ指摘しておきますが…unordered_mapsにはたくさんの種類があります。

ハッシュマップで Wikipedia Article を調べてください。どの実装が使用されたかに依存して、検索、挿入および削除の観点からの特性はかなり大きく異なるかもしれません。

そして、それがunordered_mapをSTLに追加することで私が最も心配することです。彼らは私たちがPolicyの道を進むことを疑うので、彼らは特定の実装を選ばなければならないでしょう。平均的な使用で他の場合には何もありません...

たとえば、ハッシュマップの中には線形ハッシュを持っているものがあり、ハッシュマップ全体を一度にハッシュし直すのではなく、挿入ごとに一部分をハッシュし直すことがあり、コストの削減に役立ちます。

他の例:ハッシュマップの中には単純なノードのリストをバケツに使うものもあれば、マップを使うものもあればノードを使わないで最も近いスロットを見つけるものもあります。前面にあります(キャッシュのようなものです)。

そのため、現時点ではstd::mapまたはおそらくloki::AssocVector(凍結データセット用)を好む傾向があります。

誤解しないでください、私はstd::unordered_mapを使いたいです、そして将来私はそうするかもしれません。この結果。

30
Matthieu M.

ここでは十分に言及されていない重要な違いがあります。

  • mapはすべての要素へのイテレータを安定に保ちます。C++ 17では、イテレータを無効にすることなく(そして潜在的な割り当てなしで正しく実装されていれば)要素をあるmapから他の要素に移動できます。
  • 単一の操作に対するmapのタイミングは、大きな割り当てを必要としないため、一般的により一貫性があります。
  • libstdc ++で実装されているようにunordered_mapを使用するstd::hashは信頼できない入力で供給されるとDoSに対して脆弱です(MurmurHash2を定数シードで使用します - シードが実際に役立つとは思わないので、 https://emboss.github.io/] blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded / )。
  • 順序付けされていると、効率的な範囲検索が可能になります。 42以上のキーを持つすべての要素に対して繰り返します。
18
user1531083

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

良いハッシュ関数を作るものはあなたのデータの特性に依存します。したがって、カスタムハッシュ関数を検討する予定がない場合(ただし、後で変更する可能性があります。ほとんどすべてのデータソースに対してデフォルトで適切に機能するように選択されています)。その場合は、ハッシュテーブルではなく、デフォルトでmapを使用するように、最初はmapの性質が十分に役立ちます。

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

14
Roger Pate

他の答えには理由があります。これはもう一つです。

std :: map(平衡二分木)操作は、O(log n)と最悪の場合O(log n)で償却されます。 std :: unordered_map(hash table)操作はO(1)および最悪の場合O(n)に償却されます。

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

10
Don Hatch

私は最近50000マージ&ソートを行うテストをしました。つまり、文字列キーが同じ場合は、バイト文字列をマージします。そして最終的な出力はソートされるべきです。したがって、これにはすべての挿入の検索が含まれます。

mapname__の実装では、ジョブを完了するのに200ミリ秒かかります。 unordered_map + mapname__の場合、unordered_mapの挿入には70ミリ秒、mapname__の挿入には80ミリ秒かかります。そのため、ハイブリッド実装は50ミリ秒速くなります。

mapname__を使う前に、二度考えなければなりません。プログラムの最終結果でデータをソートするだけでよい場合は、ハイブリッドソリューションの方が良い場合があります。

10
wendong

まとめ

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

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

歴史的背景

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

それがどのように実装されることができるかに関してあまりにも多くのパラメータがないので、C++がデフォルトとして順序付きマップで終わったのは広く 信じられている です。一方、ハッシュベースの実装にはたくさんのことがあります。そのため、標準化の際にグリッドロックを避けるために、順序付けされたマップを使用して、これらを たどり着いた します。 2005年頃には、多くの言語がすでにハッシュベースの実装の優れた実装を持っていたので、委員会が新しいstd::unordered_mapを受け入れるほうが簡単でした。完璧な世界では、std::mapは順不同で、別の型としてstd::ordered_mapがあります。

パフォーマンス

以下の2つのグラフは、それ自体が言えるはずです( source )。

enter image description here

enter image description here

3
Shital Shah

上記のすべてに少し追加:

要素はソートされているので、範囲ごとに要素を取得する必要がある場合はmapを使用してください。境界を越えて要素を繰り返すことができます。

0
Denis Sablukov