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

Is gcc std::unordered_map implementation slow? If so - why?

本文关键字:如果 为什么 速度慢 unordered std map gcc      更新时间:2023-10-16

我们正在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;
}

我使用了10000000SIZE,并且必须为我的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 上测试代码。

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

相关文章: