web-dev-qa-db-ja.com

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

C++でパフォーマンスが非常に重要なソフトウェアを開発しています。そこでは、並行ハッシュマップが必要であり、実装されています。そこで、同時ハッシュマップがstd::unordered_mapと比較してどれだけ遅いかを把握するためのベンチマークを作成しました。

しかし、std::unordered_mapは非常に遅いようです...これが私たちのマイクロベンチマークです(並行マップの場合、ロックが最適化されないことを確認するために新しいスレッドを作成しました。また、null値が必要なgoogle::dense_hash_mapでベンチマークを行います):

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への挿入は非常に高価なようです-他のマップでは35秒対3-5秒。また、ルックアップ時間は非常に長いようです。

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

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

そして、私の主な質問:なぜstd::unordered_mapに値を挿入するのがひどく高価なのか(最初に十分なスペースを確保しても、パフォーマンスはそれほど良くないので、再ハッシュは問題ではないようです)。

編集:

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

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

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

では、std::unordered_mapのパフォーマンスが低下する理由は何ですか?または別の質問:標準に準拠し、(ほぼ)googlesの密なハッシュマップと同程度のstd::unordered_mapインターフェイスの実装を作成できますか?または、実装者がそれを実装するために非効率的な方法を選択することを強制する規格に何かがありますか?

編集2:

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

編集3:

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

inserts: 16462
get    : 16978

Sooooooo:std::mapへの挿入よりもstd::unordered_mapへの挿入の方が速いのはなぜですか。 std::mapはローカリティが低く(ツリーと配列)、より多くの割り当てを行う必要があり(挿入ごとと再ハッシュごと+衝突ごとに〜1に加え)、最も重要なこと:アルゴリズムの複雑さ(O(logn)とO (1))!

99
Markus Pilman

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

gcc-4.7

inserts: 37728
get    : 2985

gcc-4.6を使用

inserts: 2531
get    : 1565

したがって、gcc-4.7のstd::unordered_mapは壊れています(または、Ubuntuでのgcc-4.7.0のインストールである私のインストール-およびdebianテストでのgcc 4.7.1である別のインストール)。

それまでバグレポートを提出します。gcc4.7ではstd::unordered_mapを使用しないでください!

85
Markus Pilman

Ylisarが提案したように、_unordered_map_のサイズを適切に設定していないと思います。 _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;
}
_

_10000000_のSIZEを使用しましたが、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
_

私の結論は、予想される一意の挿入の総数と等しくすること以外、初期ハッシュテーブルサイズのパフォーマンスに大きな違いはないということです。また、私があなたが観察しているパフォーマンスの違いの大きさのオーダーは見ていません。

21
jxh

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のパフォーマンスに匹敵する可能性があるため、それが当てはまる場合はGCC 4.9 std :: unordered_mapまだ壊れています。

[編集]

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

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

3
Christian Leon