gcc std :: unordered_mapの実装は遅いですか?もしそうなら-なぜですか?


100

私たちは、C ++で高性能の重要なソフトウェアを開発しています。そこで、並行ハッシュマップが必要で、それを実装しました。そこで、並行ハッシュマップがと比較してどれほど遅いかを把握するためのベンチマークを作成しましたstd::unordered_map

しかし、std::unordered_map信じられないほど遅いようです...だから、これは私たちのマイクロベンチマークです(並行マップでは、ロックが最適化されないことを確認するために新しいスレッドを生成し、私も0を挿入しないことに注意してくださいgoogle::dense_hash_map。 null値が必要です):

boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> dist(std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max());
std::vector<uint64_t> vec(SIZE);
for (int i = 0; i < SIZE; ++i) {
    uint64_t val = 0;
    while (val == 0) {
        val = dist(rng);
    }
    vec[i] = val;
}
std::unordered_map<int, long double> map;
auto begin = std::chrono::high_resolution_clock::now();
for (int i = 0; i < SIZE; ++i) {
    map[vec[i]] = 0.0;
}
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "inserts: " << elapsed.count() << std::endl;
std::random_shuffle(vec.begin(), vec.end());
begin = std::chrono::high_resolution_clock::now();
long double val;
for (int i = 0; i < SIZE; ++i) {
    val = map[vec[i]];
}
end = std::chrono::high_resolution_clock::now();
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "get: " << elapsed.count() << std::endl;

(編集:ソースコード全体はここにあります:http : //pastebin.com/vPqf7eya

の結果std::unordered_mapは次のとおりです。

inserts: 35126
get    : 2959

の場合google::dense_map

inserts: 3653
get    : 816

私たちの手で裏打ちされた並行マップ(これはロックを行いますが、ベンチマークはシングルスレッドですが、別のスポーンスレッドです):

inserts: 5213
get    : 2594

pthreadをサポートせずにベンチマークプログラムをコンパイルし、すべてをメインスレッドで実行すると、手動の同時実行マップに対して次の結果が得られます。

inserts: 4441
get    : 1180

次のコマンドでコンパイルします。

g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc

したがって、特に挿入はstd::unordered_map非常に高価であるように見えます-他のマップでは3秒から5秒ですが、35秒です。また、ルックアップ時間はかなり長いようです。

私の質問:これはなぜですか?私は誰かが尋ねるスタックオーバーフローに関する別の質問を読みました、なぜstd::tr1::unordered_map彼自身の実装より遅いのですか?最高評価の回答は、std::tr1::unordered_mapより複雑なインターフェースを実装する必要があると述べています。しかし、この引数はわかりません。concurrent_mapでバケットアプローチを使用し、バケットアプローチも使用しますstd::unordered_mapgoogle::dense_hash_mapそうではありませんstd::unordered_mapが、少なくとも、手動でバックアップした同時実行セーフバージョンよりも高速である必要がありますか?)。それとは別に、ハッシュマップのパフォーマンスを低下させる機能を強制するインターフェイスには何も表示されません...

だから私の質問:std::unordered_map非常に遅いように見えるのは本当ですか?いいえの場合:何が問題ですか?はいの場合:その理由は何ですか。

そして私の主な質問:なぜ値がstd::unordered_map非常に高価なものに挿入されているのですか(最初に十分なスペースを予約したとしても、パフォーマンスはそれほど良くありません-再ハッシュは問題ではないようです)?

編集:

まず第一に、はい、提示されたベンチマークは完璧ではありません-これは私たちが多くのことを試し、ハックにすぎないためです(たとえば、uint64intを生成するディストリビューションは実際には良いアイデアではなく、ループ内で0を除外します)一種の愚かさなどです...)。

現在ほとんどのコメントで、unordered_mapに十分なスペースを事前に割り当てることでunordered_mapを高速化できると説明しています。私たちのアプリケーションでは、これはまったく不可能です。データベース管理システムを開発していて、トランザクション中にデータ(たとえば、情報のロック)を格納するためにハッシュマップが必要です。したがって、このマップは1(ユーザーが1回の挿入とコミットを行うだけ)から数十億のエントリ(フルテーブルスキャンが発生した場合)までのすべてに対応できます。ここで十分なスペースを事前に割り当てることは不可能です(そして、最初に多くを割り当てるだけでは、多くのメモリを消費します)。

さらに、私は私の質問を十分に明確に述べていなかったことをお詫び申し上げます:私はunordered_mapを高速にすることに本当に興味がありません(Googleの高密度ハッシュマップを使用するとうまくいきます)、この大きなパフォーマンスの違いがどこから来るのか本当に理解していません。これは単なる事前割り当てではありません(十分に事前割り当てされたメモリがあっても、密なマップはunordered_mapよりも桁違いに高速です。手動でバックアップされた並行マップはサイズ64の配列で始まるため、unordered_mapよりも小さいです)。

それで、この悪いパフォーマンスの理由は何std::unordered_mapですか?または別の質問:std::unordered_map標準に準拠し、(ほぼ)グーグルの密なハッシュマップと同じ速さのインターフェイスの実装を記述できますか?または、実装者がそれを実装する非効率的な方法を選択することを強制する標準に何かありますか?

編集2:

プロファイリングにより、整数の除算に多くの時間が費やされていることがわかります。std::unordered_map配列サイズには素数を使用しますが、他の実装では2の累乗を使用します。なぜstd::unordered_map素数を使用するのですか?ハッシュが悪い場合にパフォーマンスを向上させるには?良いハッシュの場合、違いはありません。

編集3:

これらは、次の番号ですstd::map

inserts: 16462
get    : 16978

Sooooooo:への挿入std::mapよりも挿入への方が速いのはなぜstd::unordered_mapですか...つまり、WATですか?std::map局所性が低く(ツリーと配列)、より多くの割り当てを行う必要があります(挿入ごとvsリハッシュごと+ +衝突ごとに〜1)。最も重要なのは、別のアルゴリズムの複雑さ(O(logn)vs O(1))です!


1
stdのほとんどのコンテナーは、その見積もりが非常に控えめです。使用しているバケット数(コンストラクターで指定)を確認し、をより適切な見積もりに増やしますSIZE
Ylisar 2012

Intel TBBのconcurrent_hash_mapを試しましたか?threadingbuildingblocks.org/docs/help/reference/...
マッドサイエンティスト

1
@MadScientist TBBを検討しました。問題はライセンスです。これは研究プロジェクトであり、どのように公開するかはまだ不明です(ほとんどの場合、オープンソースですが、商用製品での使用を許可する場合、GPLv2は制限が厳しすぎます)。また、これは別の依存関係です。しかし、後の時点でそれを使用する可能性があるので、これまでのところ、それなしで十分に生きることができます。
Markus Pilman、2012

1
valgrindなどのプロファイラーの下で実行すると、洞察に富む場合があります。
Maxim Egorushkin、2012

1
少なくともハッシュ関数が「ランダム」である場合、ハッシュテーブルの局所性は、ツリーの局所性よりもせいぜいわずかに優れています。このハッシュ関数により、近くのアイテムが近くの時間にめったにアクセスされなくなります。唯一の利点は、ハッシュテーブル配列が1つの連続したブロックであることです。ヒープが断片化されておらず、一度にすべてのツリーを構築する場合、それはとにかくツリーに当てはまる可能性があります。サイズがキャッシュよりも大きくなると、ローカリティの違いはパフォーマンスにほとんど影響しません。
Steve314

回答:


87

私は理由を見つけました:それはgcc-4.7の問題です!!

GCC-4.7

inserts: 37728
get    : 2985

GCC-4.6

inserts: 2531
get    : 1565

そう std::unordered_map、gcc-4.7が壊れています(または、私のインストール。Ubuntuでのgcc-4.7.0のインストールと、debianテストでのgcc 4.7.1です)。

バグレポートを提出します。それまではstd::unordered_map、gcc 4.7 では使用しないでください。


その原因となる4.6からのデルタに何かありますか?
Mark Canlas

30
すでにメーリングリストに報告があります。この議論はmax_load_factor、パフォーマンスの違いにつながった処理の「修正」を指し示しているようです。
jxh

このバグのタイミングが悪い!unordered_mapを使用するとパフォーマンスが非常に低下していましたが、報告され「修正」されたことをうれしく思います。
Bo Lu

+1-BBBBBUG .. gcc-4.8.2で何が起きるのだろう
ikh

2
このバグに関する更新はありますか?それ以降のバージョンのGCC(5+)でもまだ存在しますか?
rph 2016年

21

unordered_mapYlisarが示唆したように、のサイズを適切に設定していないと思います。でチェーンが長くなりすぎるunordered_mapと、g ++実装が自動的に大きなハッシュテーブルに再ハッシュするため、パフォーマンスが大幅に低下します。私が正しく覚えている場合、unordered_mapデフォルトは(最小の素数より大きい)100です。

私はchrono自分のシステムに持っていなかったので、と計時しましたtimes()

template <typename TEST>
void time_test (TEST t, const char *m) {
    struct tms start;
    struct tms finish;
    long ticks_per_second;

    times(&start);
    t();
    times(&finish);
    ticks_per_second = sysconf(_SC_CLK_TCK);
    std::cout << "elapsed: "
              << ((finish.tms_utime - start.tms_utime
                   + finish.tms_stime - start.tms_stime)
                  / (1.0 * ticks_per_second))
              << " " << m << std::endl;
}

私はのを使用しましたSIZE10000000、私ののバージョンでは少し変更する必要がありましたboost。また、私は一致するようにハッシュテーブルのサイズを事前SIZE/DEPTHに設定しました。DEPTHはは、ハッシュの衝突によるバケットチェーンの長さの推定値です。

編集:ハワードはの最大負荷率があることコメントで私に指摘しunordered_mapています1。したがって、DEPTHコードが再ハッシュする回数を制御します。

#define SIZE 10000000
#define DEPTH 3
std::vector<uint64_t> vec(SIZE);
boost::mt19937 rng;
boost::uniform_int<uint64_t> dist(std::numeric_limits<uint64_t>::min(),
                                  std::numeric_limits<uint64_t>::max());
std::unordered_map<int, long double> map(SIZE/DEPTH);

void
test_insert () {
    for (int i = 0; i < SIZE; ++i) {
        map[vec[i]] = 0.0;
    }
}

void
test_get () {
    long double val;
    for (int i = 0; i < SIZE; ++i) {
        val = map[vec[i]];
    }
}

int main () {
    for (int i = 0; i < SIZE; ++i) {
        uint64_t val = 0;
        while (val == 0) {
            val = dist(rng);
        }
        vec[i] = val;
    }
    time_test(test_insert, "inserts");
    std::random_shuffle(vec.begin(), vec.end());
    time_test(test_insert, "get");
}

編集:

コードを変更してDEPTH、簡単に変更できるようにしました。

#ifndef DEPTH
#define DEPTH 10000000
#endif

したがって、デフォルトでは、ハッシュテーブルの最悪のサイズが選択されます。

elapsed: 7.12 inserts, elapsed: 2.32 get, -DDEPTH=10000000
elapsed: 6.99 inserts, elapsed: 2.58 get, -DDEPTH=1000000
elapsed: 8.94 inserts, elapsed: 2.18 get, -DDEPTH=100000
elapsed: 5.23 inserts, elapsed: 2.41 get, -DDEPTH=10000
elapsed: 5.35 inserts, elapsed: 2.55 get, -DDEPTH=1000
elapsed: 6.29 inserts, elapsed: 2.05 get, -DDEPTH=100
elapsed: 6.76 inserts, elapsed: 2.03 get, -DDEPTH=10
elapsed: 2.86 inserts, elapsed: 2.29 get, -DDEPTH=1

私の結論は、最初のハッシュテーブルのサイズは、一意の挿入の予想される全体数と同じにすることを除いて、パフォーマンスに大きな違いはないということです。また、あなたが観察しているパフォーマンスの違いの大きさはわかりません。


6
std::unordered_mapデフォルトの最大負荷係数は1です。そのため、バケットの初期数を除いて、DEPTHは無視されます。必要に応じて、できmap.max_load_factor(DEPTH)ます。
ハワードヒナント

@HowardHinnant:その情報をありがとう。したがって、DEPTHは無視されますが、それでも、マップがより大きなマップに再ハッシュされる頻度を制御します。回答が更新され、再び感謝します
jxh

@ user315052はい、最初に適切なサイズを設定することで改善できることはわかっていますが、私たちのソフトウェアではそれを行うことはできません(研究プロジェクト-DBMSであり、挿入量はわかりません) 0から10億の間で変動する可能性があります...)。しかし、プリアリレーションを使用しても、マップよりも遅く、Googleのデンスマップよりもはるかに遅くなります。大きな違いが何であるのか、私はまだ疑問に思っています。
Markus Pilman、2012

@MarkusPilman:SIZEあなたがどのくらいの大きさで作業​​していたのかを決して提供していないので、私の結果があなたの結果とどのように比較されるのかわかりません。私が言うことができるunordered_mapとの倍の速度であるDEPTHにセット1し、適切に事前に割り当て。
jxh 2012

1
@MarkusPilman:私の時間はすでに秒単位です。あなたの時間はミリ秒単位だと思いました。DEPTHset toを使用した挿入に1かかる時間が数3秒未満の場合、これはどの程度遅くなりますか?
jxh

3

64ビット/ AMD / 4コア(2.1GHz)コンピューターを使用してコードを実行しました。その結果、次の結果が得られました。

MinGW-W64 4.9.2:

std :: unordered_mapの使用

inserts: 9280 
get: 3302

std :: mapの使用

inserts: 23946
get: 24824

私が知っているすべての最適化フラグを備えたVC 2015:

std :: unordered_mapの使用

inserts: 7289
get: 1908

std :: mapの使用

inserts: 19222 
get: 19711

私はGCCを使用してコードをテストしていませんが、VCのパフォーマンスに匹敵する可能性があると思うので、それがtrueの場合、GCC 4.9 std :: unordered_mapはまだ壊れています。

[編集]

ですから、コメントで誰かが言ったように、GCC 4.9.xのパフォーマンスがVCのパフォーマンスに匹敵すると考える理由はありません。変更があったら、GCCでコードをテストします。

私の答えは、他の答えに対するある種の知識ベースを確立することです。


「GCCを使用してコードをテストしていませんが、VCのパフォーマンスに匹敵する可能性があると思います。」完全に根拠のない主張。元の投稿で見つかったものに匹敵するベンチマークはありません。この「答え」は、「なぜ」の質問に答えることは言うまでもなく、質問に答えることはありません。
4ae1e1 2015年

2
「GCCを使用してコードをテストしていません」... MinGWをほとんど知らずにMinGWを取得して使用したのはどうしてですか?MinGWは基本的にGCCの密接に追跡するポートです。
underscore_d 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.