【问题标题】:Why is std::tr1::unordered_map slower than a homegrown hash map?为什么 std::tr1::unordered_map 比本地哈希映射慢?
【发布时间】:2011-08-06 18:43:04
【问题描述】:

我编写了一个基本程序,它接受字符串并通过将它们插入到字符串->整数哈希映射中来计算唯一字符串的出现次数。

我使用 std::tr1::unordered_map 进行存储,为自定义哈希函数和自定义相等函数模板化。密钥类型实际上是char*,而不是太慢的std::string

然后我更改了相同的代码以使用一个非常非常简单的哈希表(实际上是一个由哈希索引的 {key, value} 结构的数组),其大小为 2 的幂,并且对冲突进行线性探测。该程序的速度提高了 33%。

考虑到当我使用 tr1::unordered_map 时,我预先调整了哈希​​表的大小,因此它永远不必增长,并且我使用完全相同的哈希和比较例程,所以 tr1::unordered_map 这样做会减慢它的速度与可以想象的最基本的哈希图相比,增加了 50%?

我在这里所说的“简单”哈希映射类型的代码:

typedef struct dataitem {
    char* item;
    size_t count;
} dataitem_t;

dataitem_t hashtable[HASHTABLE_SIZE] = {{NULL,0}}; // Start off with empty table

void insert(char* item) {
    size_t hash = generate_hash(item);
    size_t firsthash = hash;
    while (true) {
        hash &= HASHTABLE_SIZE_MASK; // Bitmasking effect is hash %= HASHTABLE_SIZE
        if (hashtable[hash].item == NULL) { // Free bucket
            hashtable[hash].item = item;
            hashtable[hash].count = 1;
            break;
        }
        if (strcmp(hashtable[hash].item, item) == 0) { // Not hash collision; same item
            hashtable[hash].count += 1;
            break;
        }
        hash++; // Hash collision.  Move to next bucket (linear probing)
        if (hash == firsthash) {
            // Table is full.  This does not happen because the presizing is correct.
            exit(1);
        }
    }
}

【问题讨论】:

  • 什么是“本土”哈希图?
  • @Nawaz:我自己用大约 20 行 C 语言编写的。
  • @Borealid:所以如果你不发布你的代码,我们怎么能说为什么std::tr1::unordered_map 比你的“本土”哈希图慢?
  • @Nawaz:我已经发布了代码。但是,就像我说的,它只是最简单的小哈希表。请记住,tr1::unordered_map 也已预先调整大小以适合数据。
  • 分析器对测试时消耗的 CPU 时间有何看法?

标签: c++ hashmap unordered-map


【解决方案1】:

您的“本地”哈希图比std::tr1::unordered_map 更快1,因为正如您自己所说,您的本地哈希图是“简单”并且它不处理检查哈希表是否已满。可能还有很多你在操作之前没有检查的东西。这可能是您的哈希映射比std::tr1::unordered_map 快的原因。

另外,std::tr1::unordered_map 的性能是由实现定义的,因此不同的实现在速度方面的表现也会不同。您可以查看它的实现并将其与您的进行比较,因为这是您可以做的第一件事,我相信这也将在一定程度上回答您的问题。

1.我只是假设你的说法是正确的,并据此说了以上的话。

【讨论】:

  • 如果是这种情况,我会很高兴,但实际上我确实检查了我测试的生产代码中的哈希表已满。它只包括存储项目的第一个哈希值。如果我们一直循环并再次获取该哈希,则表已满。由于哈希表的大小被预先设定为不需要增长,因此任何 tr1::unordered_map 用于调整大小的条件都将始终为假,因此该分支的时间复杂度无关紧要。上面的代码是不是“操作前不检查”的另一种情况?
  • 我已经用 table-is-full 检查更新了问题中的代码。
【解决方案2】:

一般来说,比较未按照相同规格构建的组件的速度是不公平的。

在不确切知道您测量的内容(即哪种操作组合、哪种负载因子以及哪种现有/不存在数据的组合)的情况下,很难解释差异来自哪里。

g++的TR1通过链式解决碰撞。这意味着动态分配。但这也可以在高负载水平下提供更好的性能。

【讨论】:

  • 所讨论的基准是加载具有随机左偏长度最多 9k 个字符的随机字符串,然后对哈希表中的所有元素进行单次迭代。
【解决方案3】:

您的“本地哈希图”根本不是哈希图,而是侵入式哈希集。 这就是它更快的原因。就这么简单。

嗯,实际上侵入式哈希集也不精确,但它是最接近的匹配。

【讨论】:

  • 上面的代码是什么使它不是哈希映射?
  • 它不是映射,因为它没有将键映射到值。它只是将键插入散列 set.
【解决方案4】:

我希望扩展@AProgrammer 的答案。

您的哈希图很简单,因为它是根据您的需要定制的。另一方面,std::tr1::unordered_map 必须完成许多不同的任务,并且在所有情况下都做得很好。这需要在所有情况下都采用平均性能方法,因此在任何特定领域都不会出色。

哈希容器的特殊之处在于有多种实现方式,您选择了开放寻址,而标准则强制实现者采用存储桶方法。两者都有不同的权衡取舍,这也是该标准这次实际上强制执行特定实现的原因之一:以便从一个库切换到另一个库时性能不会发生显着变化。在这里仅指定 Big-O 复杂度/摊销复杂度是不够的。

您说您指示unordered_map 确定决赛元素的数量,但您是否更改了负载因子?在发生冲突的情况下,链接是出了名的“糟糕”(因为缺乏内存局部性),并且使用较小的负载因子有利于分散您的元素。

最后,指出一个区别:当你调整散列图的大小时会发生什么?通过使用链接,unordered_map 不会移动内存中的元素:

  • 对它们的引用仍然有效(即使迭代器可能无效)
  • 在大型或复杂对象的情况下,不会调用复制构造函数

这与您的简单实现形成对比,后者会产生O(N) 副本(除非您使用线性重新散列来分散工作,但这绝对不简单)。

因此,unordered_map 的选择似乎是为了平滑尖峰,代价是平均插入速度较慢。

可以做一些事情:提供一个自定义分配器。通过为您的用例编写特定的分配器,并一次性分配其所有内存(因为您知道将插入多少对象,并且可以让分配器报告一个节点有多少内存)。然后以类似堆栈的方式分配节点(简单的指针增加)。它应该会(在某种程度上)提高性能。

【讨论】:

    猜你喜欢
    • 2015-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多