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
インターフェイスの実装を作成できますか?または、実装者がそれを実装するために非効率的な方法を選択することを強制する規格に何かがありますか?
プロファイリングにより、整数の除算に多くの時間が使用されることがわかります。 std::unordered_map
は配列サイズに素数を使用しますが、他の実装では2のべき乗を使用します。 std::unordered_map
が素数を使用するのはなぜですか?ハッシュが悪い場合にパフォーマンスを向上させるには?良いハッシュの場合、それは私見に違いはありません。
これらはstd::map
の番号です:
inserts: 16462
get : 16978
Sooooooo:std::map
への挿入よりもstd::unordered_map
への挿入の方が速いのはなぜですか。 std::map
はローカリティが低く(ツリーと配列)、より多くの割り当てを行う必要があり(挿入ごとと再ハッシュごと+衝突ごとに〜1に加え)、最も重要なこと:アルゴリズムの複雑さ(O(logn)とO (1))!
私は理由を見つけました:それは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
を使用しないでください!
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
_
私の結論は、予想される一意の挿入の総数と等しくすること以外、初期ハッシュテーブルサイズのパフォーマンスに大きな違いはないということです。また、私があなたが観察しているパフォーマンスの違いの大きさのオーダーは見ていません。
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のコード。
私の答えは、他の答えに対するある種の知識ベースを確立することです。