gcc std :: unordered_map的执行速度慢吗?如果是这样 - 为什么?

时间:2012-07-23 14:03:06

标签: c++ stl c++11 hashmap concurrenthashmap

我们正在用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密集哈希映射一样快?或者标准中是否有某些内容强制实施者选择低效的方式来实现它?

编辑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))!

3 个答案:

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

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