为什么LLVM选择开放寻址哈希table来实现llvm::StringMap?
Why does LLVM choose open-addressing hash table to implement llvm::StringMap?
许多消息来源说 open-addressing,llvm::StringMap
中使用的散列冲突处理方法不稳定。据说当负载因子很高(这是可以想象的)时,开放寻址不如 chaining。
但是如果负载因子低,open-addressing会有很大的内存浪费,因为我必须在内存中分配Bucket_number * sizeof(Record)字节,即使大部分buckets不保留记录。
所以我的问题是,LLVM 选择开放寻址而不是分离链接的原因是什么?仅仅是因为缓存局部性带来的速度优势(记录本身存储在桶中)吗?
谢谢:)
编辑:C++11 标准对 std::unordered_set
和 std::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
记录负载因子 F
与 N*(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 解交错到两个连续的目标数组中,有效地使操作成为缓存友好的线性扫描。
许多消息来源说 open-addressing,llvm::StringMap
中使用的散列冲突处理方法不稳定。据说当负载因子很高(这是可以想象的)时,开放寻址不如 chaining。
但是如果负载因子低,open-addressing会有很大的内存浪费,因为我必须在内存中分配Bucket_number * sizeof(Record)字节,即使大部分buckets不保留记录。
所以我的问题是,LLVM 选择开放寻址而不是分离链接的原因是什么?仅仅是因为缓存局部性带来的速度优势(记录本身存储在桶中)吗?
谢谢:)
编辑:C++11 标准对 std::unordered_set
和 std::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
记录负载因子 F
与 N*(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 解交错到两个连续的目标数组中,有效地使操作成为缓存友好的线性扫描。