gcc std::unordered_map 实现速度慢吗?如果是这样 - 为啥?
Posted
技术标签:
【中文标题】gcc std::unordered_map 实现速度慢吗?如果是这样 - 为啥?【英文标题】:Is gcc std::unordered_map implementation slow? If so - why?gcc std::unordered_map 实现速度慢吗?如果是这样 - 为什么? 【发布时间】:2012-07-21 18:45:48 【问题描述】:我们正在用 C++ 开发一个高性能的关键软件。我们需要一个并发哈希映射并实现一个。因此,我们编写了一个基准来计算我们的并发哈希映射与std::unordered_map
相比要慢多少。
但是,std::unordered_map
似乎非常慢...所以这是我们的微基准(对于并发映射,我们生成了一个新线程以确保锁定不会被优化掉,并注意我从不插入 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 秒。查找时间似乎也很长。
我的问题:为什么会这样?我在 *** 上阅读了另一个问题,有人问,为什么 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
中插入一个值如此昂贵(即使我们在开始时保留了足够的空间,它的性能也不会好很多 - 所以重新散列似乎不是问题)?
编辑:
首先:是的,提供的基准测试并非完美无缺 - 这是因为我们使用它玩了很多,它只是一个 hack(例如,用于生成整数的 uint64
分发实际上不是一个好主意,在循环中排除 0 有点愚蠢等等...)。
目前大多数 cmets 解释说,我可以通过为 unordered_map 预先分配足够的空间来使其更快。在我们的应用程序中这是不可能的:我们正在开发一个数据库管理系统,并且需要一个哈希映射来存储事务期间的一些数据(例如锁定信息)。因此,此映射可以是从 1(用户只需进行一次插入和提交)到数十亿个条目(如果发生全表扫描)的所有内容。在这里预分配足够的空间是不可能的(刚开始分配很多会消耗太多的内存)。
此外,我很抱歉,我没有足够清楚地说明我的问题:我对快速制作 unordered_map 并不感兴趣(使用谷歌的密集哈希映射对我们来说很好),我只是不明白这个巨大的性能在哪里差异从何而来。它不能只是预分配(即使有足够的预分配内存,dense map 也比 unordered_map 快一个数量级,我们手动支持的并发 map 从大小为 64 的数组开始 - 所以比 unordered_map 小)。
那么std::unordered_map
表现不佳的原因是什么?或者换一种方式问:是否可以编写一个符合标准并且(几乎)与谷歌密集哈希图一样快的@987654344@ 接口的实现?还是标准中的某些内容强制实施者选择一种低效的方式来实施?
编辑 2:
通过分析,我发现很多时间都用于整数除法。 std::unordered_map
使用素数作为数组大小,而其他实现使用 2 的幂。为什么std::unordered_map
使用素数?如果散列不好,性能更好?对于好的哈希值,恕我直言没有任何区别。
编辑 3:
这些是std::map
的号码:
inserts: 16462
get : 16978
Sooooooo:为什么插入std::map
比插入std::unordered_map
快...我的意思是WAT? std::map
具有更差的局部性(树 vs 数组),需要进行更多分配(每次插入 vs 每次重新哈希 + 每次冲突加上 ~1),最重要的是:具有另一个算法复杂度(O(logn) vs O(1 ))!
【问题讨论】:
std 中的大多数容器对他们的估计都非常保守,我会看看你正在使用的桶数(在构造函数中指定),并将其增加到更好的估计你的SIZE
。
您尝试过 Intel TBB 的 concurrent_hash_map 吗? threadingbuildingblocks.org/docs/help/reference/…
@MadScientist 我们考虑过 TBB。问题在于许可:这是一个研究项目,我们还不确定我们将如何发布它(绝对是开源的——但如果我们想允许在商业产品中使用,GPLv2 的限制太高了)。这也是另一个依赖。但也许我们会在以后使用它,到目前为止,没有它我们也可以过得很好。
在分析器下运行它,例如valgrind,可以很有见地。
哈希表中的局部性至多比树中的局部性略好,至少在哈希函数是“随机”的情况下是这样。该哈希函数可确保您很少在附近时间访问附近的项目。您拥有的唯一优势是哈希表数组是一个连续的块。无论如何,这对于树来说都是正确的,如果堆没有碎片并且您一次构建树。一旦大小大于缓存,位置上的差异对性能的影响也很小。
【参考方案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 一起使用!
【讨论】:
从 4.6 开始的 delta 中有什么会导致这种情况的吗? There is already a report in the mailing list. 讨论似乎指向“修复”max_load_factor
处理,这导致了性能差异。
这个错误的时机不好!我的 unordered_map 性能非常差,但我很高兴它已被报告并“修复”。
+1 - 太糟糕了 BBBBBUG.. 我想知道 gcc-4.8.2 会发生什么
这个错误有什么更新吗?对于更高版本的 GCC (5+) 是否仍然存在?【参考方案2】:
我猜你的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 在 cmets 中向我指出 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
我的结论是,对于任何初始哈希表大小,除了使其等于整个预期的唯一插入数之外,没有太大的性能差异。另外,我没有看到您观察到的数量级性能差异。
【讨论】:
std::unordered_map
的默认最大负载因子为 1。因此,除了存储桶的初始数量外,您的 DEPTH 将被忽略。如果需要,您可以map.max_load_factor(DEPTH)
。
@HowardHinnant:感谢您提供的信息。所以DEPTH
被忽略了,但它仍然控制地图重新散列成更大地图的频率。答案已更新,再次感谢
@user315052 是的,我知道我可以通过在开始时给它一个合理的大小来使它变得更好 - 但我不能在我们的软件中做到这一点(这是一个研究项目 - 一个 DBMS - 我可以不知道我会插入多少 - 它可以在 0 到 10 亿之间变化......)。但即使使用 preallication,它也比我们的地图慢,而且比谷歌的 dense_map 慢——我仍然想知道是什么造成了很大的不同。
@MarkusPilman:我不知道我的结果与您的结果相比如何,因为您从未提供过您使用的 SIZE
的大小。我可以说unordered_map
的速度是DEPTH
设置为1
并正确预分配的两倍。
@MarkusPilman:我的时间已经以秒为单位。我以为你的时间以毫秒为单位。如果将DEPTH
设置为1
的插入时间少于3
秒,这会慢一个数量级吗?【参考方案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 仍然是坏的。 p>
[编辑]
所以是的,正如 cmets 中有人所说,没有理由认为 GCC 4.9.x 的性能可以与 VC 性能相媲美。 当我进行更改时,我将在 GCC 上测试代码。
我的答案只是为其他答案建立某种知识库。
【讨论】:
"我没有使用 GCC 测试过代码,但我认为它可能与 VC 的性能相当。"完全没有根据的说法,没有任何与原始帖子中的基准相当的基准。这个“答案”在任何意义上都不能回答问题,更不用说回答“为什么”的问题了。 "我没有使用 GCC 测试过代码" ...您是如何在对 MinGW 知之甚少的情况下设法获取和使用它的? MinGW 基本上是 GCC 的一个密切跟踪端口。以上是关于gcc std::unordered_map 实现速度慢吗?如果是这样 - 为啥?的主要内容,如果未能解决你的问题,请参考以下文章
为啥允许 std::unordered_map::rehash() 使迭代器无效?
std::unordered_map 如何表现? [C++]