每Java文档中,哈希代码的String
对象被计算为:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用
int
算术,其中s[i]
是 我字符串的个字符,n
是字符串的长度,以及^
表示取幂。
为什么将31用作乘数?
我知道乘数应该是一个相对较大的素数。那么为什么不29或37甚至97?
每Java文档中,哈希代码的String
对象被计算为:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用
int
算术,其中s[i]
是 我字符串的个字符,n
是字符串的长度,以及^
表示取幂。
为什么将31用作乘数?
我知道乘数应该是一个相对较大的素数。那么为什么不29或37甚至97?
Answers:
根据约书亚·布洛赫(Joshua Bloch)的《有效的Java》(这本书不值得推荐,由于对stackoverflow的不断提及,我买了这本书):
选择值31是因为它是奇数质数。如果是偶数且乘法运算溢出,则信息将丢失,因为乘以2等于移位。使用质数的优势尚不清楚,但这是传统的。31的一个不错的特性是乘法可以用移位和减法代替,以获得更好的性能:
31 * i == (i << 5) - i
。现代VM自动执行这种优化。
(摘自第3章第9项:在覆盖等号时始终覆盖哈希码,第48页)
作为古德里奇和塔玛西娅指出的,如果您使用了50,000个英语单词(由Unix的两个变体中提供的单词列表组成的并集),则使用常量和41将产生少于7次的冲突在每种情况下。知道这一点,许多Java实现选择这些常量之一就不足为奇了。
碰巧的是,当我看到这个问题时,我正在阅读“多项式哈希码”部分。
编辑:这是我上面提到的〜10mb PDF书籍的链接。请参见Java数据结构和算法的 10.2哈希表(第413页)
在(大多数)旧处理器上,乘以31可能相对便宜。例如,在ARM上,它只有一条指令:
RSB r1, r0, r0, ASL #5 ; r1 := - r0 + (r0<<5)
大多数其他处理器将需要单独的移位和减法指令。但是,如果您的乘数很慢,那仍然是一个胜利。现代处理器往往具有快速乘法器,因此只要32正确,就不会有太大区别。
这不是一个很棒的哈希算法,但是它已经足够好,并且比1.0代码好(并且比1.0规范好得多!)。
String.hashCode
早于StrongARM(IIRC)引入了一个8位乘法器,并可能将带有移位运算的算术/逻辑组合增加到两个周期。
Map.Entry
已被固定由规范是key.hashCode() ^ value.hashCode()
尽管它甚至不是一个二元集合,因为key
和value
具有完全不同的含义。是的,这意味着Map.of(42, 42).hashCode()
或Map.of("foo", "foo", "bar", "bar").hashCode()
可以预期为零。因此,请勿将地图用作其他地图的键…
您可以在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可能也很容易这样做)。
实际上,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是谢尔顿·库珀的最爱号码。
在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是复合的,这让我有点紧张。
乔希
在最新版本的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,然后减去自身,因此需要廉价的资源。