许多消息来源称开放寻址,llvm::StringMap
中使用的哈希冲突处理方法并不稳定。当负载系数很高(这是可以想象的)时,开放寻址被认为不如链接。
但是如果负载因子很低,开放寻址将会浪费大量内存,因为我必须在内存中分配Bucket_number * sizeof(Record)字节,即使大多数存储桶没有记录。
所以我的问题是,LLVM选择开放寻址而不是单独链接的原因是什么?是否仅仅是因为缓存局部性所获得的速度优势(记录存储在桶本身中)?
感谢:)
编辑:std::unordered_set
和std::unordered_map
的C ++ 11标准要求意味着链接方法,而不是开放寻址。为什么LLVM选择的哈希冲突处理方法甚至不能满足C ++标准?是否有任何llvm :: StringMap的特殊用例可以保证这种偏差? (编辑:这个slide deck将几个LLVM数据结构的性能与STL数据结构的性能进行比较)
另一个问题,顺便说一句:
llvm::StringMap
如何保证密钥'增长时不会重新计算哈希值?
manual说:
哈希表增长不会重新计算表中已有的字符串的哈希值。
答案 0 :(得分:4)
让我们看一下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
个整数{1}}等效链式实现的整数。在典型的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的幂而不是素数表大小。这确保了增长表有效地使一个额外的哈希码位发挥作用并将双重表解交错为两个连续的目标阵列,从而有效地使操作成为缓存友好的线性扫描。