为什么String的hashCode()没有缓存0?

时间:2010-02-22 11:20:13

标签: java string hashcode

我注意到在Java 6的String源代码中,hashCode只缓存0以外的值。性能的差异由以下代码段显示:

public class Main{
   static void test(String s) {
      long start = System.currentTimeMillis();
      for (int i = 0; i < 10000000; i++) {
         s.hashCode();
      }
      System.out.format("Took %d ms.%n", System.currentTimeMillis() - start);
   }
   public static void main(String[] args) {
      String z = "Allocator redistricts; strict allocator redistricts strictly.";
      test(z);
      test(z.toUpperCase());
   }
}

Running this in ideone.com提供以下输出:

Took 1470 ms.
Took 58 ms.

所以我的问题是:

  • 为什么String的hashCode()没有缓存0?
  • Java字符串哈希值为0的概率是多少?
  • 对于散列为0的字符串,每次避免重新计算哈希值的性能损失的最佳方法是什么?
  • 这是缓存值的最佳实践方式吗? (即缓存所有除了一个?)

为了您的娱乐,这里的每一行都是一个散列为0的字符串:

pollinating sandboxes
amusement & hemophilias
schoolworks = perversive
electrolysissweeteners.net
constitutionalunstableness.net
grinnerslaphappier.org
BLEACHINGFEMININELY.NET
WWW.BUMRACEGOERS.ORG
WWW.RACCOONPRUDENTIALS.NET
Microcomputers: the unredeemed lollipop...
Incentively, my dear, I don't tessellate a derangement.
A person who never yodelled an apology, never preened vocalizing transsexuals.

9 个答案:

答案 0 :(得分:55)

你什么都不担心。这是一种思考这个问题的方法。

假设你有一个应用程序除了可以全年使用哈希字符串之外什么都不做。假设它需要一千个字符串,全部在内存中,以循环方式重复调用hashCode(),一百万次,然后又获得另外一千个新字符串并再次执行。

并且假设字符串的哈希码为零的可能性实际上远大于1/2 ^ 32。我确定它有点大于1/2 ^ 32,但是让我们说它比这更糟糕,比如1/2 ^ 16(平方根!现在情况更糟!)

在这种情况下,您可以从Oracle工程师那里获得更多益处,从而改善这些字符串的哈希码的缓存方式。所以你写信给他们并要求他们解决它。并且它们发挥作用,以便每当s.hashCode()为零时,它立即返回 (即使是第一次!100%的改进!)。让我们说他们这样做而不会降低任何其他情况下的性能。

万岁!现在你的应用程序是......让我们看看......快0.0015%!

过去需要一整天的时间现在只需要23小时57分48秒!

请记住,我们设定的方案是为了给出怀疑带来的每一个可能的好处,通常是一种荒谬的程度。

这对你来说是否值得?

编辑:自从几个小时前发布此消息后,我让我的一个处理器疯狂地查找具有零哈希码的双字短语。到目前为止,它提出了:bequirtle zorillo,chronogrammic schtoff,contusive cloisterlike,creashaks organzine,drumwood boulderhead,electroanalytic exercisable,以及最好的不可理解。这超出了大约2 ^ 35种可能性,因此在完美分布的情况下,我们希望只能看到8.显然,在它完成的时候,我们会有很多次,但不会更加奇怪。更重要的是,我现在想出了一些有趣的乐队名称/专辑名称!没有公平的窃取!

答案 1 :(得分:24)

它用0表示“我还没有编写哈希码”。替代方案是使用单独的布尔标志,这将占用更多内存。 (当然,根本不要缓存哈希码。)

我不希望许多字符串哈希为0;可以说,散列例程有意义地故意避免0(例如,将0的散列转换为1,并缓存该值)。这会增加碰撞,但避免重复。现在已经太晚了,因为String hashCode算法是明确记录的。

至于这是否是一个好主意:它是一种肯定有效的缓存机制,并且可能(请参阅编辑)更好地进行更改以避免重新散列最终带有哈希的值我个人有兴趣看到导致Sun首先相信这是值得做的数据 - 它为每个创建的字符串占用了额外的4个字节,但是经常或很少经常散列,并且唯一的好处用于多次散列的字符串

编辑:正如KevinB在其他地方的评论中指出的那样,上面的“避免0”建议可能会有一个净成本,因为它有助于非常罕见的案例,但是需要对每个哈希计算进行额外比较。

答案 2 :(得分:18)

我认为到目前为止其他答案都缺失了一些重要的东西:零值存在,以便hashCode-caching机制在多线程环境中可靠地运行。

如果您有两个变量,比如cachedHashCode本身和一个isHashCodeCalculated布尔值来指示是否已经计算了cachedHashCode,那么您需要线程同步才能在多线程环境中工作。同步对性能有害,特别是因为字符串在多个线程中非常常用。

我对Java内存模型的理解有点粗略,但这里大概是发生了什么:

  1. 当多个线程访问变量(如缓存的hashCode)时,无法保证每个线程都会看到最新的值。如果变量从零开始,则A更新它(将其设置为非零值),然后线程B不久后读取它,线程B仍然可以看到零值。

  2. 从多个线程访问共享值还有另一个问题(没有同步) - 您最终可能会尝试使用仅部分初始化的对象(构造对象不是原子进程)。 64位原语(如long和double)的多线程读写也不一定是原子的,因此如果两个线程尝试读取并更改long或double的值,则一个线程最终会看到一些奇怪的并且部分设置。或者类似的东西。如果你试图同时使用两个变量,比如cachedHashCode和isHashCodeCalculated,也会出现类似的问题 - 一个线程很容易出现,看到其中一个变量的最新版本,但是另一个变量的旧版本。

  3. 解决这些多线程问题的常用方法是使用同步。例如,您可以将所有对缓存的hashCode的访问权限放在synchronized块中,或者您可以使用volatile关键字(尽管请注意这一点,因为语义有点令人困惑)。

  4. 然而,同步减慢了速度。像字符串hashCode这样的坏主意。字符串经常被用作HashMaps中的键,因此您需要hashCode方法才能很好地执行,包括在多线程环境中。

  5. 32位或更少的Java原语(如int)是特殊的。与长(64位值)不同,您可以确定永远不会读取int的部分初始化值(32位)。当你在没有同步的情况下读取一个int时,你不能确定你将获得最新的设置值,但是你可以确定你得到的值是一个在某个时候由你的线程明确设置的值或另一个线程。

  6. java.lang.String中的hashCode缓存机制设置为依赖上面的第5点。您可以通过查看java.lang.String.hashCode()的源代码来更好地理解它。基本上,多个线程一次调用hashCode,hashCode最终可能会多次计算(如果计算的值为零,或者多个线程一次调用hashCode并且都看到零缓存值),但是您可以确定hashCode ()将始终返回相同的值。所以它很强大,而且性能也很高(因为没有同步可以作为多线程环境中的瓶颈)。

    就像我说的,我对Java内存模型的理解有点粗略,但我很确定我已经掌握了上述权利。最终,它是一个非常聪明的习惯用于缓存hashCode而没有同步开销。

答案 3 :(得分:8)

0未缓存,因为实现将缓存值0解释为“尚未初始化的缓存值”。替代方法是使用java.lang.Integer,其中null表示该值尚未缓存。但是,这意味着额外的存储开销。

关于String的哈希码被计算为0的概率,我会说概率非常低,并且可能在以下情况下发生:

  • String为空(尽管每次重新计算此哈希码实际上是O(1))。
  • 发生溢出,最终计算的哈希码为0(e.g. Integer.MAX_VALUE + h(c1) + h(c2) + ... h(cn) == 0)。
  • 字符串只包含Unicode字符0.非常不可能,因为这是一个控制字符,除了“纸带世界”(!)之外没有任何意义:

来自Wikipedia

  

代码0(ASCII代码名称NUL)是a   特殊情况。在纸带中,它是   没有洞的情况。它是   方便将此作为填充处理   字符没有其他意义

答案 4 :(得分:5)

这是一个很好的问题,与security vulnerability相关。

  

“当散列字符串时,Java也会缓存它   散列属性中的散列值,但仅限于结果不为零的情况。   因此,目标值零对攻击者来说特别有意义,因为它可以防止缓存   并强行重新散列。“

答案 5 :(得分:2)

十年后,情况发生了变化。老实说,我简直不敢相信(但是我内心的怪胎非常高兴)。

您已经注意到,某些字符串中的某些String::hashCodezero,而这个 并未被缓存(可以做到这一点)。很多人争辩(包括在本问答中)为什么在java.lang.String中没有添加字段,例如:hashAlreadyComputed并仅使用该字段。问题很明显:每个String实例都有多余的空间。有两个 reason java-9引入了compact String,这是很简单的事实,即许多基准测试表明这是一个相当(过度)使用的类,在大多数情况下应用程序。要添加更多空间?决定是:否。尤其是因为最小的可能加法是1 byte,而不是1 bit(对于32 bit JMV,多余的空间将是8 bytes:1用于标志,7用于对齐)。

因此,Compact String出现在java-9中,如果您仔细看(或仔细看),他们会在java.lang.String中添加字段:{{ 1}}。我不是只是反对吗?它不是那么容易。紧凑字符串的重要性似乎超出了“多余空间”的论点。同样重要的是,多余的空间仅对coder很重要(因为对齐没有间隙)。相反,在32 bits VMjdk-8的布局为:

java.lang.String

请注意此处的重要事项:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                           VALUE
  0    12          (object header)                           N/A
 12     4   char[] String.value                              N/A
 16     4      int String.hash                               N/A
 20     4          (loss due to the next object alignment)
 Instance size: 24 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

因为每个Java对象都是对齐的(取决于JVM和某些启动标志,例如Space losses : ... 4 bytes total. 的多少),所以在UseCompressedOops中有一个空白String中的未使用。因此,当添加4 bytes时,只需花费coder 而无需添加额外的空间。因此,在添加 1 byte后,布局已更改:

Compact String

java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 byte[] String.value N/A 16 4 int String.hash N/A 20 1 byte String.coder N/A 21 3 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 3 bytes external = 3 bytes total 吃了coder,差距缩小到1 byte。因此,“损坏”已在3 bytes中进行。对于jdk-932 bits JVM有所增加;对于8 bytes : 1 coder + 7 gap-没有增加,64 bit JVM占据了空白。

现在,在coder中,他们决定利用该jdk-13,因为它仍然存在。让我提醒您,具有零hashCode的String的概率为40亿分之一;仍然有人说:那又怎样?让我们解决这个问题! Voilá:gap的{​​{1}}布局:

jdk-13

这是java.lang.String。在代码库中:

java.lang.String object internals:
OFFSET  SIZE      TYPE DESCRIPTION                            VALUE
  0    12           (object header)                           N/A
 12     4    byte[] String.value                              N/A
 16     4       int String.hash                               N/A
 20     1      byte String.coder                              N/A
 21     1   boolean String.hashIsZero                         N/A
 22     2           (loss due to the next object alignment)
 Instance size: 24 bytes
 Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

等等! boolean String.hashIsZero public int hashCode() { int h = hash; if (h == 0 && !hashIsZero) { h = isLatin1() ? StringLatin1.hashCode(value) : StringUTF16.hashCode(value); if (h == 0) { hashIsZero = true; } else { hash = h; } } return h; } 字段?难道不应该这样命名:h == 0?为什么实施不像:

hashIsZero

即使我阅读了源代码下的注释:

hashAlreadyComputed

只有在我读了this之后才有意义。相当棘手,但这一次只写一次,在上面的讨论中有很多细节。

答案 6 :(得分:0)

  
      
  • 为什么String的hashCode()没有缓存0?
  •   

零值保留为“哈希码未缓存”。

  
      
  • Java字符串哈希值为0的概率是多少?
  •   

根据Javadoc,String的哈希码的公式是:

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

使用int算术,其中s[i]是字符串的第i个字符,n是字符串的长度。 (作为特殊情况,空字符串的散列被定义为零。)

我的直觉是,如上所述的哈希码函数在int值的范围内给出了String哈希值的统一传播。统一传播意味着随机生成的字符串散列为零的概率为1 ^ 2 ^ 32。

  
      
  • 对于散列为0的字符串,每次避免重新计算哈希值的性能损失的最佳方法是什么?
  •   

最好的策略是忽略这个问题。如果你反复散列相同的String值,你的算法会有一些奇怪的东西。

  
      
  • 这是缓存值的最佳实践方式吗? (即缓存所有除了一个?)
  •   

这是一个空间与时间的权衡。 AFAIK,替代方案是:

  • 为每个String对象添加一个cached标志,使每个Java String占用一个额外的单词。

  • 使用hash成员的最高位作为缓存标志。这样,您可以缓存所有哈希值,但只有一半可能的String哈希值。

  • 根本不要在字符串上缓存哈希码。

我认为Java设计师已经对Strings做出了正确的调用,我确信他们已经做了大量的分析,证实了他们决策的正确性。但是,它,这将始终是处理缓存的最佳方式。

(请注意,有两个“常用”字符串值,它们散列为零;空字符串,以及仅由NUL字符组成的字符串。但是,与成本相比,计算这些值的哈希码的成本较低计算典型字符串值的哈希码。)

答案 7 :(得分:0)

好的人,它保持0,因为如果它是零长度,它将最终为零。

并且不需要花很长时间才能发现len为零,所以哈希码也必须如此。

所以,对于你的代码审查!这就是Java 8的全部荣耀:

 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

如您所见,如果字符串为空,这将始终返回快速零:

  if (h == 0 && value.length > 0) ...

答案 8 :(得分:0)

&#34;避免0&#34;建议似乎适合作为最佳实践推荐,因为它有助于在写入之前的分支操作的微薄成本中帮助真正的问题(可在攻击者提供的可构造案例中严重意外地降低性能)。有一些剩余的意外性能下降&#39;如果进入集合的唯一东西哈希到特殊调整值,则可以行使。但这最坏的情况是2倍而不是无限制。

当然,String的实现无法改变,但没有必要使问题永久化。