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


480

每Java文档中,哈希代码String对象被计算为:

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

使用int算术,其中s[i]字符串的个字符,n是字符串的长度,以及^表示取幂。

为什么将31用作乘数?

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


1
还要比较stackoverflow.com/questions/1835976/…-如果您编写自己的hashCode函数,我认为31是一个不好的选择。
汉斯·彼得·斯托尔

6
如果是29、37或97,您会问“为什么不选择31?”
罗恩侯爵

2
@EJP重要的是要知道选择“否”背后的原因。除非数字是黑魔术的结果。
Dushyant Sabharwal'9

有一个博客帖子由@彼得- lawrey约在这里:vanilla-java.github.io/2018/08/12/...这里:vanilla-java.github.io/2018/08/15/...
克里斯托夫•鲁西

@DushyantSabharwal我的观点是,它可能 29或37或97、41 或许多其他值,而没有太大的实际差异。我们在1976
了37。–罗恩侯爵

Answers:


405

根据约书亚·布洛赫(Joshua Bloch)的《有效的Java》(这本书不值得推荐,由于对stackoverflow的不断提及,我买了这本书):

选择值31是因为它是奇数质数。如果是偶数且乘法运算溢出,则信息将丢失,因为乘以2等于移位。使用质数的优势尚不清楚,但这是传统的。31的一个不错的特性是乘法可以用移位和减法代替,以获得更好的性能:31 * i == (i << 5) - i。现代VM自动执行这种优化。

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


346
那么所有的素数都是奇数,除了2。
基普

38
我不认为Bloch之所以说选择它是因为它是一个奇数质数,而是因为它是奇数AND且因为它是质数(AND,因为它可以轻松地优化为移位/减法)。
马特b

50
31被选中是因为这是一个奇怪的素数???这没有任何意义-我说选择31是因为它提供了最佳分布-请检查 computinglife.wordpress.com/2008/11/20/…–
Computinglife

65
我认为选择31很不幸。当然,这可能会在旧计算机上节省一些CPU周期,但是在短的asi字符串上已经存在哈希冲突,例如“ @和#!或Ca和DB。如果选择例如1327144003或at至少524287这也让位位移:524287 *我==我<< 19 -岛
汉斯-彼得·斯托

15
@Jason参阅我的答案stackoverflow.com/questions/1835976/…。我的观点是:如果使用较大的质数,则碰撞将更少,并且这些天什么也不损失。如果您将非英语语言与常见的非ASCII字符一起使用,则问题会更加严重。对于许多程序员来说,编写自己的hashCode函数时,31是一个不好的例子。
汉斯·彼得·斯特尔

80

作为古德里奇和塔玛西娅指出的,如果您使用了50,000个英语单词(由Unix的两个变体中提供的单词列表组成的并集),则使用常量和41将产生少于7次的冲突在每种情况下。知道这一点,许多Java实现选择这些常量之一就不足为奇了。

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

编辑:这是我上面提到的〜10mb PDF书籍的链接。请参见Java数据结构和算法的 10.2哈希表(第413页)


6
但是请注意,如果您使用任何一种国际字符集且其通用字符超出ASCII范围,则可能会遇到更多的冲突。至少我检查了31个和德语。所以我认为31的选择是不正确的。
汉斯·彼得·斯特尔

1
@jJack,您的答案中提供的链接已损坏。
SK Venkat

此答案中的两个链接都断开了。同样,第一段中的论点是不完整的。其他奇数与您在基准测试中列出的五个相比如何?
Mark Amery

58

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

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

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

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


7
有趣的是,在我的台式机上,与31的乘法实际上比与例如9821的乘法慢一些。我猜编译器会尝试将其“优化”为移位和加法。:-)
汉斯·彼得·斯托尔2010年

1
我不认为我曾经使用过ARM,但在+/- 255范围内的所有值都不够快。使用2减1的幂具有不幸的效果,即对两个值的匹配更改将哈希码乘以2的幂。值-31会更好,并且我认为类似-83(64 + 16 + 2 + 1)的东西可能会更好(将比特变好一点)。
超级猫

@supercat不被减负说服。似乎您将回到零。/ String.hashCode早于StrongARM(IIRC)引入了一个8位乘法器,并可能将带有移位运算的算术/逻辑组合增加到两个周期。
汤姆·哈特芬

1
@ TomHawtin-tackline:使用31,四个值的哈希将为29791 * a + 961 * b + 31 * c + d;使用-31,则为-29791 * a + 961 * b-31 * c + d。我认为如果这四个项目是独立的,则差异不会很大,但如果相邻项目对成对,则产生的哈希码将是所有未配对项目的贡献,再加上32(来自配对项目)的某些倍数。对于字符串来说,可能并不太重要,但是如果编写一种用于哈希聚合的通用方法,则相邻项目匹配的情况将成比例地普遍存在。
2014年

3
@supercat有趣的事实,的哈希码Map.Entry已被固定由规范是key.hashCode() ^ value.hashCode()尽管它甚至不是一个二元集合,因为keyvalue具有完全不同的含义。是的,这意味着Map.of(42, 42).hashCode()Map.of("foo", "foo", "bar", "bar").hashCode()可以预期为零。因此,请勿将地图用作其他地图的键…
Holger

33

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

通过不使用2的幂,低位,最右边的位也会被填充,并与进入哈希的下一个数据混合。

该表达式n * 31等效于(n << 5) - n


29

您可以在http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622的 “注释”下阅读Bloch的原始推理。他针对哈希表中生成的“平均链大小”调查了不同哈希函数的性能。P(31)是他在K&R的书中发现的那段时间的常见功能之一(但即使是Kernighan和Ritchie也记不清它的来源)。最后,他基本上必须选择一个,因此他选择了P(31)它,因为它似乎表现良好。尽管P(33)还算不错,并且乘以33的速度也相当快(只需乘以5乘以加数),他还是选择了31,因为33不是素数:

在剩余的四个中,我可能会选择P(31),因为它是在RISC机器上计算的最便宜的方法(因为31是两个的两个乘方之差)。P(33)同样便宜,但是它的性能稍差一些,而33是复合的,这让我有点紧张。

因此,推理并不像这里的许多答案所暗示的那样合理。但是我们都擅长在做出直截了当的决定后提出理性的理由(甚至Bloch可能也很容易这样做)。


2
彻底的研究和公正的答案!
Vishal K

22

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

实际上,通过设置,可以用相同的速度完成与更大的素数73的乘法运算y := x + 8 * x; z := x + 8 * y

使用73或37(而不是31)可能会更好,因为它会导致代码更密集:两条LEA指令只占用6个字节,而move + shift +减法则为7个字节(乘以31)。一个可能的警告是在Intel的Sandy网桥架构上,此处使用的3参数LEA指令变慢了,增加了3个周期的延迟。

此外,73是谢尔顿·库珀的最爱号码。


5
您是Pascal程序员还是什么?:=东西是什么?
Mainguy 2012年

11
@Mainguy实际上是ALGOL语法,在伪代码中经常使用。
2013年

4
但在ARM汇编中,可以在一条指令中完成31的乘法运算
phuclv


TPOP(1999)中,可以读到有关早期Java的信息(第57页):“ ...问题解决了,用一个等效的哈希替换了我们所展示的哈希(乘数为37)...”
miku

19

尼尔·科菲(Neil Coffey)解释了为什么在消除偏差时使用31 。

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


12

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

下表针对三个数据集总结了上述各种哈希函数的性能:

1)所有在Merriam-Webster的第二本国际无删节词典中输入的单词和短语(311,141个字符串,平均长度为10个字符)。

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

3)昨晚运行了几个小时的网络爬虫收集的URL列表(28,372个字符串,平均长度49个字符)。

表中显示的性能指标是哈希表中所有元素的“平均链大小”(即,用于比较元素的键比较的期望值)。

                          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函数的两个残破版本以外,所有函数均提供出色的,几乎无法区分的性能。我强烈猜想这种性能本质上是“理论上的理想”,这是如果您使用真正的随机数生成器代替哈希函数会得到的结果。

我会排除WAIS函数,因为它的规范包含随机数的页面,并且它的性能并不比任何简单得多的函数都要好。其余六个功能中的任何一个似乎都是绝佳的选择,但我们必须选择一个。我想我会排除Vo的变体和Weinberger的功能,因为它们增加了复杂性,尽管很小。在剩余的四个中,我可能会选择P(31),因为它是在RISC机器上计算的最便宜的方法(因为31是两个的两个乘方之差)。P(33)的计算同样便宜,但是它的性能稍差一些,而33是复合的,这让我有点紧张。

乔希


5

布洛赫(Bloch)不太了解这一点,但是我一直听到/相信的基本原理是,这是基本的代数。哈希归结为乘法和模运算,这意味着如果可以的话,永远不要使用具有公因子的数字。换句话说,相对质数提供了均匀的答案分布。

使用散列组成的数字通常为:

  • 您放入的数据类型的模数(2 ^ 32或2 ^ 64)
  • 哈希表中存储区计数的模数(可变。在Java中以前是质数,现在是2 ^ n)
  • 在您的混音功能中乘以或乘以魔数
  • 输入值

您实际上只能控制其中几个值,因此需要多加注意。


4

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

哈希字符串的目的是

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

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

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


3

我不确定,但是我猜想他们测试了一些质数样本,发现31在某些可能的字符串样本中的分布最佳。


1

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

31 * i == (i << 5) - i
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.