[英]Is gcc std::unordered_map implementation slow? If so - why?
我們正在用C ++開發一個高性能的關鍵軟件。 我們需要一個並發的哈希映射並實現一個。 所以我們寫了一個基准來弄清楚,我們的並發哈希映射與std::unordered_map
相比要慢多少。
但是, std::unordered_map
似乎非常慢......所以這是我們的微基准測試(對於並發映射,我們產生了一個新的線程,以確保鎖定不會被優化掉,並注意我從來沒有因為我因為我還可以使用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秒vs 3-5秒。 查找時間似乎也很高。
我的問題:這是為什么? 我在stackoverflow上讀了另一個問題,有人問,為什么std::tr1::unordered_map
比他自己的實現慢。 有最高級別的答案狀態, std::tr1::unordered_map
需要實現更復雜的接口。 但是我看不出這個論點:我們在concurrent_map中使用了bucket方法, std::unordered_map
使用了bucket-approach( google::dense_hash_map
沒有,但是std::unordered_map
應該至少和我們的手一樣快)支持並發安全版本?)。 除此之外,我在界面中看不到強制使哈希映射表現不佳的功能的任何內容......
所以我的問題是: std::unordered_map
似乎很慢嗎? 如果不是:出了什么問題? 如果是:這是什么原因。
而我的主要問題是:為什么在std::unordered_map
插入一個值如此可怕的昂貴(即使我們在開始時保留了足夠的空間,它也沒有更好的表現 - 所以重新散列似乎不是問題)?
首先:是的,所提出的基准測試並不完美 - 這是因為我們玩了很多它並且它只是一個黑客(例如生成int的uint64
發行版在實踐中不是一個好主意,在一個中排除0循環是一種愚蠢的等...)。
目前大多數評論都解釋說,我可以通過為它預分配足夠的空間來加快unordered_map的速度。 在我們的應用程序中,這是不可能的:我們正在開發一個數據庫管理系統,並且需要一個哈希映射來在事務期間存儲一些數據(例如鎖定信息)。 因此,這個映射可以是從1(用戶只進行一次插入和提交)到數十億條目(如果發生全表掃描)的所有內容。 在這里預先分配足夠的空間是不可能的(並且在開始時分配很多將消耗太多內存)。
此外,我很抱歉,我沒有說清楚我的問題:我對制作unordered_map並不感興趣(使用googles密集哈希映射對我們來說很好),我只是不明白這個巨大的性能差異來自何處。 它不能只是預分配(即使有足夠的預分配內存,密集映射比unordered_map快一個數量級,我們的手動並發映射以大小為64的數組開始 - 所以比unordered_map小一個)。
那么std::unordered_map
這種糟糕表現的原因是什么? 或者有不同的問題:是否可以編寫std::unordered_map
接口的實現,該接口是標准符合和(幾乎)與googles密集哈希映射一樣快? 或者標准中是否有某些內容強制實施者選擇低效的方式來實現它?
通過分析,我看到很多時間用於整數divions。 std::unordered_map
使用素數作為數組大小,而其他實現使用2的冪。 為什么std::unordered_map
使用素數? 如果散列是壞的,要更好地執行? 對於好的哈希,它確實沒有任何區別。
這些是std::map
的數字:
inserts: 16462
get : 16978
Sooooooo:為什么插入std::map
比插入std::unordered_map
更快...我的意思是WAT? std::map
有一個更糟糕的局部性(樹與數組),需要進行更多的分配(每次插入vs每次重新加上+每次碰撞加1次),最重要的是:有另一種算法復雜度(O(logn)vs 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)。
我將提交錯誤報告..直到那時:不要使用std::unordered_map
和gcc 4.7!
我猜你沒有正確調整你的unordered_map
大小,正如Ylisar建議的那樣。 當鏈在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
是由於哈希沖突導致的桶鏈長度的估計。
編輯: Howard在評論中指出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上測試代碼。
我的答案只是為其他答案建立某種知識基礎。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.