哈希码是针对String
对象计算的。
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用int
算术,在此情况下,其中s [i]
是字符串的第i个字符,n
是字符串的长度,^
表示指数。
为什么使用31作为乘数?
我知道乘数应该是一个相对较大的质数。那么为什么不选用29、37或甚至是97呢?
哈希码是针对String
对象计算的。
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用int
算术,在此情况下,其中s [i]
是字符串的第i个字符,n
是字符串的长度,^
表示指数。
为什么使用31作为乘数?
我知道乘数应该是一个相对较大的质数。那么为什么不选用29、37或甚至是97呢?
31 * i == (i << 5) - i
。现代虚拟机会自动进行此类优化。”在(大多数)旧处理器上,乘以31可能相对便宜。例如,在ARM上,它只是一条指令:
RSB r1, r0, r0, ASL #5 ; r1 := - r0 + (r0<<5)
大多数其他处理器需要单独的移位和减法指令,但是如果您的乘法器很慢,这仍然是一种胜利。现代处理器往往具有快速的乘法器,因此只要32位进入正确的位置,就不会有太大的区别。
它不是一个很好的哈希算法,但足够好并且比1.0代码(以及1.0规范)好得多。
String.hashCode
的诞生早于 StrongARM 处理器,据我所知,它引入了一个 8 位乘法器,并可能将组合算术/逻辑与移位操作增加到了两个周期。 - Tom Hawtin - tacklineMap.Entry
的哈希码已被固定为 key.hashCode() ^ value.hashCode()
,即使它甚至不是一个无序对,因为 key
和 value
具有完全不同的含义。是的,这意味着 Map.of(42, 42).hashCode()
或 Map.of("foo", "foo", "bar", "bar").hashCode()
等的哈希码可以可预测地为零。因此,不要将映射用作其他映射的键... - Holger通过乘法,位被向左移动,这使用哈希码的可用空间更多,减少碰撞。
不使用2的幂次方时,低阶的右侧位也会被填充,以与进入哈希的下一段数据混合。
表达式n * 31
等同于(n << 5) - n
。
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个字节,而使用31进行乘法则需要7个字节的移位加减运算。可能的一个注意点是,这里使用的3参数LEA指令在英特尔Sandy Bridge架构上变得更慢了,延迟增加了3个周期。
此外,73是Sheldon Cooper最喜欢的数字。
从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是合数,这让我有点担心。
Josh
Java字符串hashCode()方法和31
这是因为数字31有一个好的特性——它的乘法可以被位移操作替换,而位移操作比标准的乘法更快:
31 * i == (i << 5) - i