出于性能原因,是否总是需要将哈希表的桶数设为质数?

问题描述

https://www.quora.com/Why-should-the-size-of-a-hash-table-be-a-prime-number?share=1

我看到有人提到哈希表的桶数最好是质数。

总是这样吗?当哈希值已经均匀分布时,就不需要使用质数了吗?

https://github.com/rui314/chibicc/blob/main/hashmap.c

例如上面的哈希表代码没有使用素数作为桶的数量

https://github.com/rui314/chibicc/blob/main/hashmap.c#L37

但是哈希值是使用 fnv_hash 从字符串生成的。

https://github.com/rui314/chibicc/blob/main/hashmap.c#L17

那么使用不一定是质数的桶大小是有道理的吗?

解决方法

templatetypedef 一如既往地有一些出色的地方 - 只需添加更多和一些示例...

出于性能原因,是否总是需要将哈希表的桶数设为质数?

没有。首先,将质数用于桶计数往往意味着您需要花费更多的 CPU 周期来将哈希函数返回的哈希值折叠/修改为当前的桶计数。一种流行的替代方法是对桶数使用 2 的幂(例如 8、16、32、64……当您调整大小时),因为这样您就可以执行按位 AND 运算以从哈希值映射到 1 中的桶CPU 周期。 回答了您的“那么使用不一定是质数的桶大小是有道理的?”

调整哈希表的性能通常意味着权衡更强的哈希函数和质数修改的成本与更高冲突的成本。

当散列函数无法为其馈送的键生成非常好的分布时,主桶计数通常有助于减少冲突。

例如,如果您使用标识散列(基本上,将指针地址转换为 double)散列一堆指向 64 位 size_t 的指针,那么散列值都将是8 的倍数(由于对齐),如果你有一个哈希表大小,比如 1024 或 2048(2 的幂),那么你所有的指针都会散列到桶索引的 1/8(特别是桶 0、8、 16、25、32 等)。对于质数的桶,至少指针值 - 如果负载因子很高,则不可避免地会散布在比桶索引范围大得多的范围内 - 往往会环绕散列表,命中不同的索引。

当您使用非常强大的散列函数时 - 低位有效地随机但可重复,无论桶数如何,您都已经在桶之间获得了良好的分布。有时,即使使用非常弱的散列函数 - 例如身份散列 - h(x) == x - 密钥中的所有位都非常随机,以至于它们产生的分布与加密散列可以产生的分布一样好,所以没有点在更强的哈希上花费额外的时间 - 这甚至可能增加冲突。

有时分布本身并不是很好,但您可以负担得起使用额外内存来保持低负载因子,因此不值得使用素数或更好的散列函数。尽管如此,额外的存储桶也会给 CPU 缓存带来更多压力 - 因此事情最终可能比预期的要慢。

其他时候,具有身份散列的键具有落入不同桶的内在趋势(例如,因为它们可能是由递增计数器生成的,即使某些值不再使用)。在这种情况下,强大的散列函数会增加冲突并恶化 CPU 缓存访问模式。无论您使用二的幂还是主要的桶计数在这里都没有什么区别。

当hash值已经均匀分布时,就不需要使用质数了吗?

如果你在讨论 mod-to-current-hash-table-size 操作之后的哈希值,那句话是微不足道的,但有点毫无意义:即使在那里的分布也与很少的冲突直接相关。

如果您谈论更有趣的情况,即哈希值均匀分布在哈希函数返回类型值空间(例如 64 位整数)中,那么在将这些值修改为当前哈希表存储桶计数之前,那么直到素数有帮助的空间,但只有当散列键空间比散列桶索引更大的范围时。上面的指针示例说明了:如果你说 800 个不同的 8 字节对齐的指针进入 ~1000 桶,那么数字最低的指针和更高的地址之间的差异至少是 799*8 = 6392...你'在桌子周围至少环绕了 6 次以上(对于尽可能接近的指针),并且质数的桶会增加每次“环绕”修改到以前未使用过的几率桶。

请注意,上述对素数桶计数的一些好处适用于任何类型的冲突处理 - 单独链接、线性探测、二次探测、双重哈希、布谷鸟哈希、罗宾汉哈希等。