我们正在用C ++开发一个高性能的关键软件。我们需要一个并发的哈希映射并实现一个。所以我们写了一个基准来弄清楚,我们的并发哈希映射与std::unordered_map
相比要慢多少。
但是,std::unordered_map
似乎非常慢......所以这是我们的微基准测试(对于并发映射,我们产生了一个新的线程,以确保锁定不会被优化掉,并注意到我从来没有inser 0因为我也使用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秒。查找时间似乎也很高。
我的问题:这是为什么?我在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
如此可怕的昂贵(即使我们在开始时保留足够的空间,它也不会表现得更好 - 所以重新打算似乎不是问题) ?
首先:是的,所提出的基准测试并不完美 - 这是因为我们玩了很多它并且它只是一个黑客(例如生成整数的uint64
分布实际上不是一个好主意,在循环中排除0是一种愚蠢的等等......)。
目前大多数评论都解释说,我可以通过为它预分配足够的空间来加快unordered_map的速度。在我们的应用程序中,这是不可能的:我们正在开发一个数据库管理系统,并且需要一个哈希映射来在事务期间存储一些数据(例如锁定信息)。因此,这个映射可以是从1(用户只进行一次插入和提交)到数十亿条目(如果发生全表扫描)的所有内容。在这里预先分配足够的空间是不可能的(并且在开始时分配很多将消耗太多内存)。
此外,我道歉,我没有说清楚我的问题:我真的不想快速制作unordered_map(使用googles密集哈希映射对我们来说很好),我只是不明白这个巨大的性能在哪里差异来自。它不能只是预分配(即使有足够的预分配内存,密集映射比unordered_map快一个数量级,我们的手动并发映射以大小为64的数组开始 - 所以比unordered_map小一些。)
那么std::unordered_map
表现不佳的原因是什么?或者有不同的问题:是否可以编写std::unordered_map
接口的实现,该接口是标准符合和(几乎)与googles密集哈希映射一样快?或者标准中是否有某些内容强制实施者选择低效的方式来实现它?
通过分析我发现很多时间用于整数除法。 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))!
答案 0 :(得分:85)
我找到了原因:这是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一起使用!
答案 1 :(得分:21)
正如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;
}
我使用了SIZE
10000000
,并且我的版本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
我的结论是,除了使其等于整个预期的唯一插入数之外,任何初始哈希表大小都没有太大的性能差异。此外,我没有看到您正在观察的数量级性能差异。
答案 2 :(得分: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的性能相当,所以如果这是真的,那么GCC 4.9 std :: unordered_map 它仍然会被破坏。
<强> [编辑] 强>
所以是的,正如有人在评论中所说,没有理由认为GCC 4.9.x的表现可与VC表现相媲美。 当我进行更改时,我将在GCC上测试代码。
我的回答只是为其他答案建立某种知识基础。