为什么LLVM选择开放寻址哈希table来实现llvm::StringMap?

Why does LLVM choose open-addressing hash table to implement llvm::StringMap?

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

但是如果负载因子低,open-addressing会有很大的内存浪费,因为我必须在内存中分配Bucket_number * sizeof(Record)字节,即使大部分buckets不保留记录。

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

谢谢:)

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

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

llvm::StringMap如何保证键的散列值在增长时不被重新计算? manual 表示:

hash table growth does not recompute the hash values for strings already in the table.

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

有效:

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

除了容量是动态的,负载系数似乎保持在容量的 37.5% 到 75% 之间。

对于 N 记录负载因子 FN*(1+1/F) 指针相比,这会产生 N/F 指针加上 N/F 用于开放地址实现的整数加上 N 整数用于等效的链式实现。在典型的 64 位系统上,开放地址版本介于 ~4% 和 ~30% 较小.

之间

然而,正如您所怀疑的那样,这里的主要优势在于缓存效果。除了平均缓存通过缩小数据来减少争用之外,冲突过滤归结为对连续 32 位哈希键的线性重新探测,而不检查任何进一步的信息。因此,拒绝冲突 比链式情况更快,在这种情况下,必须遵循 link 进入可能未缓存的存储,因此可以使用明显更高的加载因子。另一方面,必须在指针查找中采取额外的可能缓存未命中 table,但这是一个常数,不会随着相当于一次链式冲突的负载而降低。

有效:

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 中的进一步优化是使用二次幂而不是素数 table 大小。这确保了增加 table 有效地带来了一个额外的哈希码位发挥作用,并将加倍的 table 解交错到两个连续的目标数组中,有效地使操作成为缓存友好的线性扫描。