102

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

しかし、std::unordered_map信じられないほど遅いようです...これが私たちのマイクロベンチマークです(並行マップでは、ロックが最適化されないようにするために新しいスレッドを生成しました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非常にコストがかかるようです.35秒対他のマップの3〜5秒. また、検索時間もかなり長いようです。

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

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

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

編集:

まず第一に:はい、提示されたベンチマークは完璧ではありません-これは、私たちがそれでたくさん遊んだためであり、単なるハックです(たとえば、uint64intを生成する配布は実際には良い考えではなく、ループで0を除外しますちょっとばかげているなど...)。

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

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

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

編集2:

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

編集3:

の数値は次のstd::mapとおりです。

inserts: 16462
get    : 16978

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

4

3 に答える 3

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_mapgcc 4.7 と一緒に使用しないでください!

于 2012-07-24T15:54:16.847 に答える
21

unordered_mapYlisar が提案したように、あなたは のサイズを適切に設定していないと推測しています。でチェーンが長くなりすぎるunordered_mapと、g++ の実装は自動的により大きなハッシュ テーブルに再ハッシュし、これはパフォーマンスに大きな影響を与えます。私の記憶が正しければ、unordered_mapデフォルトは (smallest prime greater than)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、私のバージョンの に合わせて少し変更する必要がありましたboostSIZE/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

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

于 2012-07-23T15:12:44.340 に答える
2

I have run your code using a 64 bit / AMD / 4 cores (2.1GHz) computer and it gave me the following results:

MinGW-W64 4.9.2:

Using std::unordered_map:

inserts: 9280 
get: 3302

Using std::map:

inserts: 23946
get: 24824

VC 2015 with all the optimization flags I know:

Using std::unordered_map:

inserts: 7289
get: 1908

Using std::map:

inserts: 19222 
get: 19711

I have not tested the code using GCC but I think it may be comparable to the performance of VC, so if that is true, then GCC 4.9 std::unordered_map it's still broken.

[EDIT]

So yes, as someone said in the comments, there is no reason to think that the performance of GCC 4.9.x would be comparable to VC performance. When I have the change I will be testing the code on GCC.

My answer is just to establish some kind of knowledge base to other answers.

于 2015-11-16T22:54:41.150 に答える