gcc std::unordered_map 实施速度慢吗?如果是这样 - 为什么
Is gcc std::unordered_map implementation slow? If so - why?
我们正在C++开发一款高性能的关键软件。在那里,我们需要一个并发哈希映射并实现一个。因此,我们编写了一个基准测试来计算,与std::unordered_map
相比,我们的并发哈希映射慢了多少。
但是,std::unordered_map
似乎非常慢...所以这是我们的微基准测试(对于并发映射,我们生成了一个新线程,以确保锁定不会得到优化,并注意我从不坚持 0,因为我也使用 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 秒。此外,查找时间似乎相当长。
我的问题是:这是为什么?我读到另一个关于stackoverflow的问题,有人问,为什么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
中插入一个值如此昂贵(即使我们在开始时保留了足够的空间,它的性能也没有好多少 - 所以重新散列似乎不是问题(?
编辑:
首先:是的,所呈现的基准测试并非完美无缺 - 这是因为我们玩了很多,它只是一个黑客(例如,生成整数的uint64
分布在实践中不是一个好主意,在循环中排除 0 有点愚蠢等等......
目前,大多数评论都解释说,我可以通过为它预先分配足够的空间来使unordered_map更快。在我们的应用程序中,这是不可能的:我们正在开发一个数据库管理系统,需要一个哈希映射来存储事务期间的一些数据(例如锁定信息(。因此,此映射可以是从 1(用户只需进行一次插入并提交(到数十亿个条目(如果发生全表扫描(的所有内容。在这里预分配足够的空间是不可能的(一开始只是分配很多会消耗太多内存(。
此外,我很抱歉,我没有足够清楚地陈述我的问题:我对unordered_map快速并不感兴趣(使用谷歌的密集哈希图对我们来说效果很好(,我只是真的不明白这种巨大的性能差异来自哪里。它不能只是预分配(即使有足够的预分配内存,密集映射也比unordered_map快一个数量级,我们的手动支持的并发映射从大小为 64 的数组开始 - 所以比 unordered_map 小(。
那么std::unordered_map
表现不佳的原因是什么呢?或者不同的问题:是否可以编写符合标准且(几乎(与谷歌密集哈希图一样快的std::unordered_map
接口的实现?或者标准中是否有某些内容迫使实施者选择一种低效的方式来实施它?
编辑2:
通过分析,我看到很多时间都用于整数。 std::unordered_map
使用质数作为数组大小,而其他实现则使用 2 的幂。std::unordered_map
为什么使用质数?如果哈希不好,表现得更好?对于好的哈希,恕我直言,它没有区别。
编辑3:
这些是std::map
的数字:
inserts: 16462
get : 16978
那么:为什么插入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(。
在那之前,我将提交错误报告..:不要在gcc 4.7中使用std::unordered_map
!
我猜你没有像伊利萨尔建议的那样正确调整你的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 上测试代码。
我的答案只是为其他答案建立某种知识库。
- 如果"new int"返回"int*",那么为什么"new int[n]"不返回"int**"?
- 如果有一个模板构造函数只有一个泛型参数,为什么我必须有一个复制构造函数
- 如果'C'公开继承'B',B 私下继承'A',为什么我不能在"C"中创建"A"的对象?
- 如果全局变量默认是外部变量,为什么要添加"extern"关键字?
- 如果初始值设定项不为真,为什么会这样?
- 如果整个应用程序是虚拟映射的,为什么 new 会进行系统调用?
- 为什么如果我添加这一行,我的程序会不断询问值
- 如果 x.h 仅由函数声明组成,为什么有必要在 x 中包含 x.h.cpp
- 如果我重新定义 sqrt 函数,为什么使用 std::sqrt 失败?
- 如果 const 不分配内存,为什么我可以获取 const 的地址?
- 为什么 WinInet 在通过 FQDN 连接时无法通过协商自动进行身份验证,但如果通过 IP 连接则成功?
- 如果我的手机是 ARMv8,为什么 Android Studio 会C++编译为 ARMv7?
- 在C++中,如果我可以直接将整数分配给指针而不使用"new",为什么要使用"new"?
- C++ 如果在 if 为 true 之后运行,为什么还会这样做
- 为什么 bool 和 _Bool 如果它们在内存中占用 1 个字节,它们只能存储 0 或 1
- 如果我注释掉换行符,为什么'string'会成为一个不合格的变量
- 在 C++ 中声明 const 对象需要用户定义的默认构造函数.如果我有一个可变成员变量,为什么不呢?
- 如果 iostream 对象不可复制,为什么以下代码是合法的?
- 如果不包含 pthread,为什么 GCC 的线程标准库实现会抛出异常?
- 如果我在下面的代码片段中添加"delete[] d;",为什么我得到零?