为啥 Java 的 String 中的 hashCode() 使用 31 作为乘数?
Posted
技术标签:
【中文标题】为啥 Java 的 String 中的 hashCode() 使用 31 作为乘数?【英文标题】:Why does Java's hashCode() in String use 31 as a multiplier?为什么 Java 的 String 中的 hashCode() 使用 31 作为乘数? 【发布时间】:2010-09-22 21:27:38 【问题描述】:根据 Java 文档,String
对象的 hash code 计算为:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用
int
算术,其中s[i]
是 字符串的第 i 个字符,n
是字符串的长度 字符串,^
表示取幂。
为什么要用 31 作为乘数?
我知道乘数应该是一个比较大的素数。那么为什么不是 29、37 甚至 97?
【问题讨论】:
也比较***.com/questions/1835976/… - 如果您编写自己的 hashCode 函数,我认为 31 是一个糟糕的选择。 如果是 29、37 甚至 97,你会问“为什么不是 31?” @EJP 了解选择否的原因很重要。除非这个数字是黑魔法的结果。 @peter-lawrey 有一篇关于它的博客文章:vanilla-java.github.io/2018/08/12/… 和这里:vanilla-java.github.io/2018/08/15/… @DushyantSabharwal 我的意思是它可能一直是 29 或 37 或 97 或 41 或许多其他值,而不会产生太大的实际差异。 1976 年我们使用的是 37。 【参考方案1】:Java String hashCode() 和 31
这是因为 31 有一个很好的属性——它的乘法可以被比标准乘法更快的按位移位代替:
31 * i == (i << 5) - i
【讨论】:
【参考方案2】:Goodrich 和 Tamassia 从超过 50,000 个英语单词(由两个 Unix 变体中提供的单词列表的并集)计算得出,使用常量 31、33、37、39 和 41 将在每个单词中产生少于 7 个冲突案子。这可能是很多 Java 实现选择这样的常量的原因。
请参阅Data Structures and Algorithms in Java 的第 9.2 节哈希表(第 522 页)。
【讨论】:
但是请注意,如果您使用任何类型的国际字符集以及 ASCII 范围之外的常见字符,您可能会遇到更多的冲突。至少,我检查了这个 31 和德语。所以我认为31的选择是错误的。【参考方案3】:哈希函数的一个很大的期望是,它们的结果的均匀随机性能够在诸如hash(x) % N
这样的操作中幸存下来,其中 N 是任意数(在许多情况下,是 2 的幂),一个原因是此类操作被普遍使用在用于确定槽的哈希表中。在计算哈希时使用素数乘数会降低乘数和 N 共享除数的概率,这会使运算结果的随机性降低。
其他人指出乘以 31 可以通过乘法和减法来完成。我只想指出,这样的素数有一个数学术语:Mersenne Prime
所有的梅森素数都是二的幂次方,所以我们可以写成:
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 似乎通过用移位和子替换它们来优化梅森素数的乘法,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)。
【讨论】:
【参考方案4】:在最新版本的 JDK 中,仍然使用 31。 https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/String.html#hashCode()
哈希字符串的用途是
唯一(在hashcode计算文档中查看运算符^
,它有助于唯一)
计算成本低廉
31 是最大值,可放入 8 位(= 1 字节)寄存器,最大素数可放入 1 字节寄存器,为奇数。
乘以 31 是
【讨论】:
【参考方案5】:您可以在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 是 2 的两个幂的差)。 P(33) 是 计算起来同样便宜,但它的性能略差,并且 33是复合的,有点紧张。
因此,推理并不像这里的许多答案所暗示的那样合理。但我们都很擅长在做出直觉决定后提出合理的理由(甚至 Bloch 也可能倾向于这样做)。
【讨论】:
【参考方案6】:来自JDK-4045622,其中 Joshua Bloch 描述了选择特定(新)String.hashCode()
实现的原因
下表总结了各种哈希的性能 上述函数,针对三个数据集:
1) 韦氏词典中包含条目的所有单词和短语 第二个国际未删节词典(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 是 复合,这让我有点紧张。
乔什
【讨论】:
【参考方案7】:实际上,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 个字节,而 move+shift+subtract 则需要 7 个字节乘以 31。一个可能的警告是,这里使用的 3 参数 LEA 指令在英特尔的 Sandy 桥架构上变得更慢,延迟增加了 3 个周期。
此外,73 是 Sheldon Cooper 最喜欢的号码。
【讨论】:
@Mainguy 这实际上是 ALGOL 语法,在伪代码中经常使用。 但是在 ARM 汇编中乘以 31 可以在一条指令中完成 @Mainguy In pseudo code what does := mean? 在TPOP (1999) 中,人们可以阅读早期 Java(第 57 页):“...问题已通过将哈希替换为与我们展示的相同的哈希(使用37) 的乘数 ..."【参考方案8】:Neil Coffey explains 为什么在消除偏见下使用 31。
基本上使用 31 可以为哈希函数提供更均匀的置位概率分布。
【讨论】:
【参考方案9】:布洛赫并没有深入探讨这一点,但我一直听到/相信的基本原理是,这是基本代数。哈希归结为乘法和取模运算,这意味着如果可以提供帮助,您永远不想使用具有公因数的数字。换句话说,相对素数提供了一个均匀分布的答案。
使用哈希组成的数字通常是:
您放入的数据类型的模数 (2^32 或 2^64) 哈希表中桶数的模数(变化。在 java 中曾经是素数,现在是 2^n) 在混音函数中乘以或移位一个幻数 输入值您实际上只能控制其中几个值,因此需要格外小心。
【讨论】:
【参考方案10】:在(大部分)旧处理器上,乘以 31 可能相对便宜。例如,在 ARM 上,它只有一条指令:
RSB r1, r0, r0, ASL #5 ; r1 := - r0 + (r0<<5)
大多数其他处理器需要单独的移位和减法指令。但是,如果您的乘数很慢,这仍然是一个胜利。现代处理器往往具有快速乘法器,因此只要 32 在正确的一侧,它就不会产生太大的影响。
这不是一个很好的哈希算法,但它已经足够好并且比 1.0 代码更好(并且比 1.0 规范好得多!)。
【讨论】:
有趣的是,在我的台式机上乘以 31 实际上比乘以 92821 慢一点。我猜编译器会尝试将其“优化”为移位和加法。 :-) 我认为我从来没有使用过 ARM,它的所有值都在 +/-255 范围内时速度不快。使用 2 减去 1 的幂具有不幸的效果,即对两个值的匹配更改会以 2 的幂更改散列码。 -31 的值会更好,我认为像 -83 (64+16+2+1) 这样的值可能会更好(更好地混合位)。 @supercat 不相信减号。看来你会回到零。 /String.hashCode
早于 StrongARM,IIRC 引入了一个 8 位乘法器,并可能增加到两个周期,用于组合算术/逻辑与移位操作。
@TomHawtin-tackline:使用 31,四个值的哈希值将是 29791*a + 961*b + 31*c + d;使用 -31,它将是 -29791*a + 961*b - 31*c + d。如果四个项目是独立的,我认为差异不会很大,但如果相邻项目对匹配,则生成的哈希码将是所有未配对项目的贡献,加上 32 的一些倍数(来自配对项目)。对于字符串,它可能无关紧要,但如果要编写一种用于散列聚合的通用方法,则相邻项匹配的情况将非常普遍。
@supercat 有趣的是,Map.Entry
的哈希码已被规范固定为 key.hashCode() ^ value.hashCode()
,尽管它甚至不是无序对,因为 key
和 value
完全不同意义。是的,这意味着Map.of(42, 42).hashCode()
或Map.of("foo", "foo", "bar", "bar").hashCode()
等可以预见为零。所以不要使用地图作为其他地图的键......【参考方案11】:
通过乘法,位向左移动。这会使用更多可用的哈希码空间,从而减少冲突。
由于不使用 2 的幂,低位的最右边位也会被填充,以便与进入散列的下一条数据混合。
表达式n * 31
等价于(n << 5) - n
。
【讨论】:
【参考方案12】:根据 Joshua Bloch 的 Effective Java (这本书推荐不够,我买了这本书,感谢 *** 上的不断提及):
选择值 31 是因为它是一个奇数素数。如果它是偶数并且乘法溢出,则信息将丢失,因为乘以 2 相当于移位。使用素数的优势不太明显,但它是传统的。 31 的一个很好的属性是乘法可以用移位和减法代替以获得更好的性能:
31 * i == (i << 5) - i
。现代虚拟机自动进行这种优化。
(来自第 3 章,第 9 项:覆盖等于时始终覆盖哈希码,第 48 页)
【讨论】:
所有素数都是奇数,除了 2。就说吧。 我不认为布洛赫说它被选中是因为它是一个奇怪的素数,而是因为它是奇数并且因为它是素数(并且因为它可以很容易地优化为移位/减法) . 31 被选中,因为它是一个奇怪的素数???这没有任何意义 - 我说选择 31 是因为它提供了最佳分布 - 检查computinglife.wordpress.com/2008/11/20/… 我认为选择 31 是相当不幸的。当然,它可能会在旧机器上节省一些 CPU 周期,但是您已经在短的 ascii 字符串(如 "@ 和 #! ,或 Ca 和 DB 上存在哈希冲突。如果您选择例如 1327144003 或 at至少 524287 也允许位移: 524287 * i == i @Jason 查看我的回答 ***.com/questions/1835976/… 。我的观点是:如果你使用更大的素数,你会得到更少的碰撞,而且这些天不会有任何损失。如果您使用具有常见非 ascii 字符的非英语语言,问题会更严重。在编写自己的 hashCode 函数时,31 对许多程序员来说是一个坏例子。【参考方案13】:我不确定,但我猜他们测试了一些素数样本,发现 31 在一些可能的字符串样本上给出了最佳分布。
【讨论】:
以上是关于为啥 Java 的 String 中的 hashCode() 使用 31 作为乘数?的主要内容,如果未能解决你的问题,请参考以下文章
java中的main方法为啥接受无效的String args
java中的String类型的对象为啥可以自动转换成Object类型的?而Object却要强制转换成String类型的