gcc std :: unordered_map实现慢吗?如果是这样-为什么?


100

我们正在用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秒。而且查找时间似乎很长。

我的问题:这是为什么?我读了另一个关于stackoverflow的问题,有人问,为什么std::tr1::unordered_map比自己的实现要慢。有最高评分的回答状态,即std::tr1::unordered_map需要实现更复杂的接口。但是我看不到这种说法:我们在并发映射中std::unordered_map使用了存储桶方法,也使用了存储桶方法(google::dense_hash_map不是,但是std::unordered_map应该至少比手工备份并发安全版本还快吗?)。除此之外,我在界面中看不到任何强制执行一项功能的功能,该功能使哈希映射的性能下降。

所以我的问题是:这真的std::unordered_map很慢吗?如果不是:那是什么问题?如果是:原因是什么。

我的主要问题是:为什么要在std::unordered_map如此昂贵的价格中插入一个值(即使我们一开始就保留了足够的空间,它的性能也不会好得多-因此重新哈希似乎不是问题)?

编辑:

首先:是的,提出的基准并不是完美无缺的-这是因为我们在基准上进行了很多uint64尝试,并且仅仅是一个hack(例如,实际上,生成int 的分布不是一个好主意,在循环中排除0有点愚蠢等等...)。

目前大多数评论都说明,我可以通过为unordered_map预先分配足够的空间来使其更快。在我们的应用程序中这是不可能的:我们正在开发一个数据库管理系统,并且需要一个哈希映射来存储事务期间的一些数据(例如锁定信息)。因此,此映射可以是从1(用户仅执行一次插入并提交)到数十亿个条目(如果发生全表扫描)的所有内容。根本不可能在这里预分配足够的空间(而刚开始分配太多会消耗太多内存)。

此外,我很抱歉,我的问题还不够清楚:我对快速提高unordered_map(使用Google密集型哈希图对我们来说没什么用)并不感兴趣,我只是不太了解这种巨大的性能差异来自何处。它不能只是预分配(即使具有足够的预分配内存,密集映射也比unordered_map快一个数量级,我们的手动并发映射以大小为64的数组开始-因此比unordered_map小)。

那么,造成这种不良表现的原因是std::unordered_map什么?或提出不同的要求:是否可以编写std::unordered_map标准符合且(几乎)与Google密集哈希图一样快的接口的实现?还是在标准中有某种东西可以强迫实施者选择一种低效的实施方式?

编辑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每次哈希+每次碰撞加〜1),并且最重要的是:具有另一种算法复杂性(O(logn)vs O(1))!


1
std中的大多数容器对它们的估计都非常保守,我来看看您正在使用的存储区数量(在构造函数中指定),然后将其增加到更好的估计值SIZE
伊利萨尔(Ylisar)2012年

您是否尝试过Intel TBB的current_hash_map?threadingbuildingblocks.org/docs/help/reference/...
MadScientist

1
@MadScientist我们考虑了TBB。问题是许可:这是一个研究项目,我们尚不确定如何发布它(最肯定是开源的,但是如果我们想允许在商业产品中使用,则GPLv2的限制太多)。这也是另一个依赖项。但是也许我们会在以后的某个时间使用它,到目前为止,如果没有它,我们可以过得很好。
Markus Pilman

1
在分析器(例如valgrind)下运行它可能会很有见识。
Maxim Egorushkin 2012年

1
至少在哈希函数为“随机”的情况下,哈希表中的局部性最多比树中的局部性好一点。该哈希函数可确保您在附近的时间很少访问附近的项目。您拥有的唯一好处是,哈希表数组是一个连续的块。无论如何,对于一棵树来说,这都是正确的,如果堆没有碎片,而您一次构建了所有树。一旦大小大于缓存,则位置差异对性能几乎没有影响。
2012年

Answers:


87

我找到了原因:这是gcc-4.7的问题!!

使用gcc-4.7

inserts: 37728
get    : 2985

使用gcc-4.6

inserts: 2531
get    : 1565

因此,std::unordered_map在gcc-4.7中已损坏(或者我的安装是Ubuntu上的gcc-4.7.0安装-在debian测试中是gcc 4.7.1的另一个安装)。

我将提交一个错误报告..在此之前:不要std::unordered_map与gcc 4.7一起使用!


4.6中的增量中有什么会导致这种情况?
Mark Canlas 2012年

30
邮件列表中已经有一个报告。讨论似乎指向max_load_factor处理的“修复” ,这导致了性能上的差异。
jxh 2012年

这个错误的时机不好!unordered_map的性能非常差,但是我很高兴它已被报告并“修复”。
博禄

+1-真是糟透了BBBBBUG ..我想知道gcc-4.8.2
ikh

2
此错误有任何更新吗?更高版本的GCC(5+)仍然存在吗?
rph

21

我猜测您没有unordered_map按照Ylisar的建议调整大小。当chain在中增长太长时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;
}

我使用SIZE10000000,并且不得不针对我的版本进行一些更改boost。还要注意,我将哈希表的大小预先设置为match SIZE/DEPTH,其中DEPTH是由于哈希冲突导致的存储桶链长度的估计值。

编辑:霍华德指出了我的意见,对于最大负载系数unordered_map1。因此,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

我的结论是,除了使初始哈希表大小等于唯一插入的整体预期数目之外,任何初始哈希表大小都没有太大的性能差异。另外,我没有看到您观察到的数量级性能差异。


6
std::unordered_map的默认最大负载系数为1。因此,除了初始的存储桶数外,您的DEPTH将被忽略。如果需要,您可以map.max_load_factor(DEPTH)
Howard Hinnant 2012年

@HowardHinnant:感谢您提供该信息。因此,会DEPTH被忽略,但是它仍然控制将地图重新​​映射为更大地图的频率。答案已经更新,再次感谢
jxh 2012年

@ user315052是的,我知道我可以通过在开始时给它一个合理的大小来做得更好-但我不能在我们的软件中做到这一点(这是一个研究项目-DBMS-而且我不知道要插入多少-它可以在0到10亿之间变化...)。但是,即使有了预分配,它也比我们的地图慢,也比Google的density_map慢-我仍然想知道那是什么与众不同。
Markus Pilman

@MarkusPilman:我不知道我的结果与您的结果相比如何,因为您从未提供过SIZE要处理的结果。我可以说设置为并正确预分配unordered_map是的两倍。DEPTH1
jxh 2012年

1
@MarkusPilman:我的时间已经以秒为单位。我以为您的时间以毫秒为单位。如果将DEPTH设置为的插入1时间少于3几秒钟,这会慢一个数量级吗?
jxh 2012年

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上测试代码。

我的答案只是为其他答案建立某种知识库。


“我尚未使用GCC测试过该代码,但我认为它可能与VC的性能相当。” 完全没有根据的声明,没有任何基准可与原始帖子中的相比。这个“答案”在任何意义上都不能回答问题,更不用说回答“为什么”问题了。
4ae1e1

2
“我还没有使用GCC测试过代码” ...您如何在不了解MinGW的情况下设法获得和使用MinGW?MinGW从根本上来说是GCC的紧密跟踪端口。
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.