【问题标题】:Why does LLVM choose open-addressing hash table to implement llvm::StringMap?为什么 LLVM 选择开放寻址哈希表来实现 llvm::StringMap?
【发布时间】:2018-01-05 08:33:54
【问题描述】:

许多消息来源说,llvm::StringMap 中使用的哈希冲突处理方法 open-addressing 不稳定。当负载因子很高(这是可以想象的)时,据说开放寻址不如链接

但是如果负载因子低,开放寻址会造成巨大的内存浪费,因为我必须在内存中分配 Bucket_number * sizeof(Record) 个字节,即使大多数桶没有记录。

所以我的问题是,LLVM 选择开放寻址而不是分离链接的原因是什么?仅仅是因为本地缓存在速度上的优势(记录本身存储在存储桶中)吗?

谢谢:)

编辑:C++11 标准对 std::unordered_setstd::unordered_map 的要求暗示了链接方法,而不是开放寻址。为什么 LLVM 会选择一个连 C++ 标准都达不到的哈希冲突处理方式? llvm::StringMap 是否有任何特殊用例可以保证这种偏差? (编辑:slide deck 比较了几种 LLVM 数据结构与 STL 对应数据结构的性能)

另一个问题,顺便说一句:

llvm::StringMap 如何保证键的哈希值在增长时不会被重新计算? manual 说:

哈希表增长不会重新计算表中已有字符串的哈希值。

【问题讨论】:

  • 想必字符串不是一个大对象(只是指针+大小),所以这样你不会浪费太多内存。对于最后一个问题:也许他们将哈希值存储在表中。这样,如果字符串不匹配,比较会更快。
  • “甚至不能满足 C++ 标准”是吧?它不是unordered_mapset。它并没有试图满足特定 std 容器的要求...
  • @Yakk 但是如果它不能满足标准的要求,用户可以使用 std::unordered_map<:string ..> 代替 llvm::StringMap。所以我问了一些特殊的用例来证明:)
  • @geza 哦,你是对的,llvm::StringRef 只占 2 个字的长度。我的问题是普遍应用这种技术,在这种情况下 sizeof(record) 可能要大得多。
  • 是的,如果 sizeof 记录很大,可能不值得。但在这种情况下,你“只是”浪费内存。它可能会更快,因为不需要指针取消引用。提示:您可以制作一个链式哈希表,其中有一个模板参数。因此,您可以选择如何存储链的第一个元素,这样您就可以对两种方式进行基准测试,并选择哪个更快

标签: c++ data-structures hash hashmap llvm


【解决方案1】:

让我们看看the implementation。在这里,我们看到该表存储为指向记录的间接指针的并行数组以及任何缓存的 32 位哈希码数组,即单独的结构数组。

有效地:

struct StringMap {
 uint32_t hashcode[CAPACITY];
 StringMapEntry *hashptr[CAPACITY];
};

除了容量是动态的并且负载率似乎维持在容量的 37.5% 到 75% 之间。

对于N 记录负载因子F,这将产生N/F 指针加上N/F 整数用于开放寻址实现,而N*(1+1/F) 指针加上N 整数用于等效链式实现。在典型的 64 位系统上,开放地址版本的大小在 ~4% 到 ~30% 之间。

但是,正如您正确怀疑的那样,这里的主要优势在于缓存效果。除了通过缩小数据平均缓存减少争用之外,冲突过滤归结为对连续 32 位哈希键的线性重新探测,而无需检查任何进一步的信息。因此,拒绝冲突要快得多,在这种情况下,链接必须跟随到可能未缓存的存储中,因此可以使用显着更高的负载因子。另一方面,必须在指针查找表上附加一个可能的缓存未命中,但这是一个常数,不会随着负载等效于一次链式冲突而降级。

有效地:

StringMapEntry *StringMap::lookup(const char *text) {
    for(uint32_t *scan = &hashcode[hashvalue % CAPACITY]; *scan != SENTINEL; ++scan) {
        uint32_t hash_value = hash_function(text);
        if(hash_value == *scan) {
            StringMapEntry *entry = p->hashptr[scan - hashcode];
            if(!std::strcmp(entry->text, text))
                return entry;
            }
        }
    }
}

加上包装等细微之处。

至于您的第二个问题,优化是预先计算和存储哈希键。这会浪费一些存储空间,但会阻止检查可能很长的可变长度字符串的昂贵操作,除非几乎可以确定匹配。而在退化的情况下,复杂的模板名称修改可能是数百个字符。

RehashTable 中的进一步优化是使用 2 的幂而不是素数表大小。这确保了表的增长有效地使一个额外的哈希码位发挥作用,并将加倍表解交错为两个连续的目标数组,有效地使该操作成为缓存友好的线性扫描。

【讨论】:

    猜你喜欢
    • 2011-02-03
    • 1970-01-01
    • 2012-02-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-29
    • 2017-05-11
    • 2011-11-17
    相关资源
    最近更新 更多