为什么String中的Java hashCode()使用31作为乘数?

时间:2008-11-18 16:39:44

标签: java string algorithm hash

根据Java文档,String对象的hash code计算如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     

使用int算术,其中s[i]为。{1}}    字符串的字符,n是字符串的长度    字符串,^表示取幂。

为什么31用作乘数?

我知道乘数应该是一个相对较大的素数。那么为什么不是29,或37,甚至97?

13 个答案:

答案 0 :(得分:372)

根据Joshua Bloch的Effective Java(一本不能推荐的书,以及我因堆栈溢出不断提及而购买的书):

  

选择值31是因为它是奇数素数。如果它是偶数并且乘法溢出,则信息将丢失,因为乘以2相当于移位。使用素数的优势不太明显,但它是传统的。 31的一个很好的属性是乘法可以用移位和减法代替,以获得更好的性能:31 * i == (i << 5) - i。现代虚拟机会自动执行此类优化。

(来自第3章,第9项:覆盖等于时始终覆盖哈希码,第48页)

答案 1 :(得分:76)

正如Goodrich and Tamassia指出的那样,如果你接受超过50,000个英语单词(形成为两个Unix变体中提供的单词列表的联合),使用常量31,33,37,39和41将在每种情况下产生少于7次碰撞。知道这一点,许多Java实现选择其中一个常量应该不足为奇。

巧合的是,当我看到这个问题时,我正在阅读“多项式哈希码”部分。

编辑:这里是我上面提到的~10mb PDF书的链接。请参见Data Structures and Algorithms in Java

的10.2哈希表(第413页)一节

答案 2 :(得分:55)

在(大多数)旧处理器上,乘以31可能相对便宜。例如,在ARM上,它只有一条指令:

RSB       r1, r0, r0, ASL #5    ; r1 := - r0 + (r0<<5)

大多数其他处理器需要单独的移位和减法指令。但是,如果你的乘数很慢,这仍然是一个胜利。现代处理器往往具有快速乘法器,因此只要32位在正确的一侧,它就没有太大的区别。

这不是一个很好的哈希算法,但它比1.0代码更好,更好(并且比1.0规范好得多!)。

答案 3 :(得分:28)

通过相乘,位向左移位。这使用了更多可用的哈希码空间,减少了冲突。

通过不使用2的幂,也会填充低位,最右边的位,以便与进入散列的下一个数据混合。

表达式n * 31相当于(n << 5) - n

答案 4 :(得分:24)

你可以在&#34;评论&#34;下阅读布洛赫的原始推理。在http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622。他研究了不同散列函数在得到的平均链尺寸方面的表现。在哈希表中。 P(31)是他在K&amp; R的书中找到的那个时期的常见功能之一(但即使是Kernighan和Ritchie也不记得它来自哪里)。最后他基本上不得不选择一个,所以他选择P(31),因为它似乎表现得很好。即使P(33)并没有真正变差,乘以33也同样快速计算(只是换了5和加法),他选择了31,因为33不是素数:

  

其余的   四,我可能选择P(31),因为它是在RISC上计算最便宜的   机器(因为31是两个两个幂的差异)。 P(33)是   同样便宜的计算,但它的表现略差,并且   33是复合的,这让我有点紧张。

因此,推理并不像这里的许多答案所暗示的那样理性。但是,在我们做出决定之后,我们总是能够提出合理的理由(甚至布洛赫也可能会这样做)。

答案 5 :(得分:22)

实际上,37会很好用! z:= 37 * x可以计算为y := x + 8 * x; z := x + 4 * y。这两个步骤都对应于一个LEA x86指令,因此速度非常快。

事实上,通过设置y := x + 8 * x; z := x + 8 * y,可以以相同的速度乘以更大的素数 73

使用73或37(而不是31)可能会更好,因为它会导致更密集的代码:两个LEA指令只占用6个字节而7个字节用于移动+移位+减法一个可能的警告是,这里使用的3参数LEA指令在英特尔Sandy桥架构上变慢,延迟增加3个周期。

此外,73是Sheldon Cooper最喜欢的号码。

答案 6 :(得分:19)

Neil Coffey explains为什么在中使用31来消除偏见

基本上使用31可以为哈希函数提供更均匀的设置位概率分布。

答案 7 :(得分:7)

来自JDK-4045622,其中Joshua Bloch描述了选择特定(新)String.hashCode()实施的原因

  

下表总结了各种哈希的性能   上述功能,用于三个数据集:

     

1)所有在Merriam-Webster中都有条目的单词和短语          2nd Int&#39; l Unabridged Dictionary(311,141个字符串,平均10个字符)。

     

2)/ bin / ,/ usr / bin / ,/ usr / lib / ,/ usr / ucb / 中的所有字符串          和/ usr / openwin / bin / *(66,304个字符串,平均长度为21个字符)。

     

3)网络抓取工具收集的网址列表,其中包含多个网址          昨晚(28,372弦,平均49个字符)。

     

表中显示的性能指标是&#34;平均链尺寸&#34;   哈希表中的所有元素(即,期望的值)   密钥数比较查找元素。)

                          Webster's   Code Strings    URLs
                          ---------   ------------    ----
Current Java Fn.          1.2509      1.2738          13.2560
P(37)    [Java]           1.2508      1.2481          1.2454
P(65599) [Aho et al]      1.2490      1.2510          1.2450
P(31)    [K+R]            1.2500      1.2488          1.2425
P(33)    [Torek]          1.2500      1.2500          1.2453
Vo's Fn                   1.2487      1.2471          1.2462
WAIS Fn                   1.2497      1.2519          1.2452
Weinberger's Fn(MatPak)   6.5169      7.2142          30.6864
Weinberger's Fn(24)       1.3222      1.2791          1.9732
Weinberger's Fn(28)       1.2530      1.2506          1.2439
     

看看这张表,它清楚所有的功能除了   当前的Java函数和Weinberger的两个破碎版本   功能提供优秀,几乎无法区分的性能。一世   强烈推测这种表现基本上是这样的   &#34;理论上的理想&#34;,如果你使用了真正的随机,那就是你得到的   数字生成器代替哈希函数。

     

我排除了WAIS功能,因为它的规范包含随机数页,其性能并不比任何一个都好。   更简单的功能。其余六个功能中的任何一个看起来都像   出色的选择,但我们必须选择一个。我想我排除了   Vo的变体和Weinberger的功能因为它们的增加   复杂性,尽管很小。在剩下的四个中,我可能会选择   P(31),因为它是在RISC机器上计算最便宜的(因为31   是两个权力的差异两个)。 P(33)同样便宜   计算,但它的表现略差,而33   复合,这让我有点紧张。

     

约什

答案 8 :(得分:4)

我不确定,但我猜他们会测试一些质数样本,并发现31对可能的字符串样本进行了最佳分布。

答案 9 :(得分:4)

布洛赫并没有深入研究这个问题,但我一直听到/相信的理由是这是基本的代数。哈希值可归结为乘法和模数运算,这意味着如果可以提供帮助,您永远不会想要使用具有公共因子的数字。换句话说,相对素数提供了均匀的答案分布。

使用哈希构成的数字通常为:

  • 您输入的数据类型的模数 (2 ^ 32或2 ^ 64)
  • 哈希表中的桶数的模数(各不相同。在java曾经是素数,现在是2 ^ n)
  • 在混音函数中乘以或移动幻数
  • 输入值

你真的只能控制其中的几个值,所以需要额外注意。

答案 10 :(得分:1)

在最新版本的JDK中,仍使用31。 https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#hashCode()

哈希字符串的目的是

  • 唯一(在哈希码计算文档中,请参见运算符^,它有助于唯一)
  • 便宜的计算费用

31是最大值,可以放入8位(= 1字节)寄存器。最大的质数可以放入1个字节的寄存器中,是奇数。

乘以31是<< 5,然后减去自身,因此需要廉价的资源。

答案 11 :(得分:0)

这是因为31具有很好的属性-它的乘法可以用按位移位来代替,该移位比标准乘法快:

31 * i == (i << 5) - i

答案 12 :(得分:0)

对散列函数的最大期望是,其结果的均匀随机性可以幸存于诸如hash(x) % N之类的操作中,其中N为任意数(在许多情况下为2的幂),一个原因是这样的操作是通常在哈希表中用于确定广告位。在计算哈希值时使用质数乘数会降低乘数和N个除数的可能性,这将使运算结果的均匀性降低。

其他人指出了很好的性质,即乘以31可以通过相乘和相减来完成。我只想指出,这类素数有一个数学术语:Mersenne Prime

所有mersenne质数都小于2的幂,因此我们可以将它们写为:

p = 2^n - 1

x乘以p:

x * p = x * (2^n - 1) = x * 2^n - x = (x << n) - x

在许多机器上,移位(SAL / SHL)和减法(SUB)通常比乘法(MUL)更快。参见instruction tables from Agner Fog

这就是为什么GCC似乎通过用mersenne质数see here替换乘积来优化乘法的原因。

但是,在我看来,这样小的素数对于哈希函数来说是一个糟糕的选择。使用相对较好的哈希函数,您将期望在哈希的较高位具有随机性。但是,使用Java哈希函数,较短的字符串在较高位几乎没有随机性(而在较低位则仍然存在高度可疑的随机性)。这使得构建有效的哈希表更加困难。参见this nice trick you couldn't do with the Java hash function

一些答案​​提到他们认为31可以适合一个字节是很好的。这实际上是没有用的,因为:

(1)我们执行移位而不是乘法,因此乘法器的大小无关紧要。

(2)据我所知,没有特定的x86指令将8字节的值与1字节的值相乘,因此即使您要进行乘法运算,您仍然需要将“ 31”转换为8字节的值。请参见here,将整个64位寄存器相乘。

(而127实际上是可以容纳一个字节的最大梅森素数。)

较小的值会增加中低位的随机性吗?也许吧,但这似乎也大大增加了可能发生的碰撞:)。

一个可以列出许多不同的问题,但是它们通常归结为两个不能很好实现的核心原则:Confusion and Diffusion

但是很快吗?可能是因为它没有太大作用。但是,如果性能确实是这里的重点,则每个循环一个字符效率很低。对于较长的字符串like this,为什么不每次循环迭代一次4个字符(8个字节)?好吧,这与当前的哈希定义很难实现,在哈希的定义中,您需要分别乘以每个字符(请告诉我是否有一点技巧可以解决这个问题:D)。