为什么在HashMap.clear()中不再使用Arrays.fill()?

我注意到在HashMap.clear()的实现中有一些奇怪的东西。 这就是它在OpenJDK 7u40中的样子 :

 public void clear() { modCount++; Arrays.fill(table, null); size = 0; } 

这就是OpenJDK 8u40的外观 :

 public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } } 

我知道,现在的table可以是空的地图,因此在本地variables中的附加检查和caching是必需的。 但为什么Arrays.fill()replace为for循环?

看来这个改变是在这个提交中引入的。 不幸的是我没有find解释为什么一个简单的for循环可能比Arrays.fill()更好。 它快吗? 还是更安全?

我会尽量总结三个在评论中提出的更合理的版本。

@霍尔说 :

我想这是为了避免类java.util.Arrays得到加载作为这种方法的副作用。 对于应用程序代码,这通常不是一个问题。

这是最容易testing的事情。 让我们编译这样的程序:

 public class HashMapTest { public static void main(String[] args) { new java.util.HashMap(); } } 

java -verbose:class HashMapTest运行它。 这将打印类加载事件发生。 使用JDK 1.8.0_60,我看到超过400个类加载:

 ... 155 lines skipped ... [Loaded java.util.Set from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.AbstractSet from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.Collections$EmptySet from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.Collections$EmptyList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.Collections$EmptyMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.Collections$UnmodifiableCollection from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.Collections$UnmodifiableList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.Collections$UnmodifiableRandomAccessList from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded sun.reflect.Reflection from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] **[Loaded java.util.HashMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.HashMap$Node from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.Class$3 from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.Class$ReflectionData from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.Class$Atomic from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded sun.reflect.generics.repository.AbstractRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded sun.reflect.generics.repository.GenericDeclRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded sun.reflect.generics.repository.ClassRepository from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.Class$AnnotationData from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded sun.reflect.annotation.AnnotationType from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.util.WeakHashMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.ClassValue$ClassValueMap from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.reflect.Modifier from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded sun.reflect.LangReflectAccess from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] [Loaded java.lang.reflect.ReflectAccess from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] **[Loaded java.util.Arrays from C:\Program Files\Java\jre1.8.0_60\lib\rt.jar] ... 

正如你所看到的, HashMap早在应用程序代码加载之前就已经加载了,而ArraysHashMap之后只加载了14个类。 HashMap负载由sun.reflect.Reflection初始化触发,因为它具有HashMap静态字段。 Arrays加载可能是由实际上有Arrays.fillWeakHashMap加载触发的。 WeakHashMap加载由java.lang.ClassValue$ClassValueMap触发,它扩展了WeakHashMapClassValueMap存在于每个java.lang.Class实例中。 所以对我来说似乎没有Arrays类JDK不能被初始化。 此外, Arrays静态初始化器非常短,它只是初始化断言机制。 这个机制被用在许多其他类中(包括很早被加载的java.lang.Throwable )。 在java.util.Arrays不执行其他静态初始化步骤。 因此@Holger版本似乎不正确的我。

在这里我们也发现很有意思的事情。 WeakHashMap.clear()仍然使用Arrays.fill 。 当它出现在那里时很有意思,但不幸的是这发生在史前时代 (在第一个公开的OpenJDK仓库中已经存在)。

接下来,@MarcoTopolnik 说 :

当然不是更安全,但fill呼叫不内联, tab短的情况下可能会更快。 在HotSpot上,循环和显式fill调用都会导致快速编译器的内在(在一个愉快的日子里)。

对于我来说, Arrays.fill并不是直接内在的(参见@apangin生成的内在列表 )。 似乎这样的循环可以被JVM识别和向量化,而不需要显式的内部处理。 所以,在特定的情况下,额外的调用是不能内联的(例如,如果达到了MaxInlineLevel限制)。 另一方面,这是非常罕见的情况,它只是一个单一的调用,它不是一个内部循环的调用,而是一个静态的,而不是虚拟/接口调用,因此性能的改善只能在一些特定的情况下是微不足道的。 不是JVM开发者通常关心的东西。

还应该注意的是,即使是C1'客户端'编译器(1-3层)也能够内联Arrays.fill ,例如,在WeakHashMap.clear()中作为内联日志( -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining )说:

 36 3 java.util.WeakHashMap::clear (50 bytes) !m @ 4 java.lang.ref.ReferenceQueue::poll (28 bytes) @ 17 java.lang.ref.ReferenceQueue::reallyPoll (66 bytes) callee is too large @ 28 java.util.Arrays::fill (21 bytes) !m @ 40 java.lang.ref.ReferenceQueue::poll (28 bytes) @ 17 java.lang.ref.ReferenceQueue::reallyPoll (66 bytes) callee is too large @ 1 java.util.AbstractMap::<init> (5 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 9 java.lang.ref.ReferenceQueue::<init> (27 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 10 java.lang.ref.ReferenceQueue$Lock::<init> (5 bytes) unloaded signature classes @ 62 java.lang.Float::isNaN (12 bytes) inline (hot) @ 112 java.util.WeakHashMap::newTable (8 bytes) inline (hot) 

当然,它也可以通过智能而强大的C2“服务器”编译器轻松embedded。 所以我在这里看不出什么问题。 似乎@Marco版本也不正确。

最后,我们从@StuartMarks(JDK开发人员,因此也有官方声音) 发表了一些评论 :

有趣。 我的直觉是这是一个错误。 此更改集的审阅线程位于此处 ,并引用此处继续的较早的线程 。 早期的线程中的初始消息指向Doug Lea的CVS存储库中的HashMap.java原型。 我不知道这是从哪里来的。 它似乎不匹配OpenJDK历史上的任何东西。

…无论如何,这可能是一些旧的快照; for循环在clear()方法中已经有很多年了。 Arrays.fill()调用是由这个变更集引入的,所以在树中只有几个月的时间。 还要注意,由这个变更集引入的基于Integer.highestOneBit()的两次幂运算也同时消失了,尽pipe这在注释中被注意到但是被忽略了。 嗯。

事实上, HashMap.clear()包含循环多年,在2013年4月10日被 Arrays.fill 取代 ,直到9月4日讨论的提交被引入时,停留时间less于半年。 讨论的提交实际上是对HashMap内部进行重大修改以解决JDK-8023463问题。 有一个长长的故事,有可能中毒HashMap的密钥具有复制hashcodes减lessHashMapsearch速度线性使其易受DoS攻击。 解决这个问题的尝试在JDK-7中进行,包括一些String hashCode的随机化。 所以看起来HashMap实现是从早期的提交中分离出来的,独立开发,然后合并到主分支中,覆盖介于两者之间的几个变化。

我们可以支持这个假设进行差异化。 取出Arrays.fill被删除的版本 (2013-09-04),并与之前的版本 (2013-07-30)进行比较。 diff -U0输出有4341行。 现在让我们来添加Arrays.fill (2013-04-01)之前的版本 。 现在diff -U0只包含2680行。 因此,新版本实际上比直接父母更类似于老版本。

结论

所以最后我会同意斯图尔特·马克斯的看法。 没有具体的理由去除Arrays.fill ,只是因为中间的变化被错误覆盖了。 在JDK代码和用户应用程序中使用Arrays.fill是完全正确的,例如,在WeakHashMapArrays类在JDK初始化过程中很早就加载了,具有非常简单的静态初始化程序, Arrays.fill方法甚至可以通过客户端编译器轻松内联,因此不应该注意性能缺陷。

因为它快得多!

我对两种方法的削减版本进行了一些彻底的基准testing:

 void jdk7clear() { Arrays.fill(table, null); } void jdk8clear() { Object[] tab; if ((tab = table) != null) { for (int i = 0; i < tab.length; ++i) tab[i] = null; } } 

在包含随机值的各种大小的数组上操作。 以下是(典型的)结果:

 Map size | JDK 7 (sd)| JDK 8 (sd)| JDK 8 vs 7 16| 2267 (36)| 1521 (22)| 67% 64| 3781 (63)| 1434 ( 8)| 38% 256| 3092 (72)| 1620 (24)| 52% 1024| 4009 (38)| 2182 (19)| 54% 4096| 8622 (11)| 4732 (26)| 55% 16384| 27478 ( 7)| 12186 ( 8)| 44% 65536| 104587 ( 9)| 46158 ( 6)| 44% 262144| 445302 ( 7)| 183970 ( 8)| 41% 

以下是在用空值填充的数组上操作时的结果(所以垃圾回收问题已经根除):

 Map size | JDK 7 (sd)| JDK 8 (sd)| JDK 8 vs 7 16| 75 (15)| 65 (10)| 87% 64| 116 (34)| 90 (15)| 78% 256| 246 (36)| 191 (20)| 78% 1024| 751 (40)| 562 (20)| 75% 4096| 2857 (44)| 2105 (21)| 74% 16384| 13086 (51)| 8837 (19)| 68% 65536| 52940 (53)| 36080 (16)| 68% 262144| 225727 (48)| 155981 (12)| 69% 

数字是纳秒, (sd)是1个标准差,表示为结果的百分比(fyi,“正态分布”人口的SD为68), vs是相对于JDK 7的JDK 8时间。

有趣的是,它不仅速度显着提高,而且偏差也稍微缩小,这意味着JDK 8的实现提供了稍微更一致的性能。

testing在jdk 1.8.0_45上运行,在随机Integer对象填充的数组上运行数百万次。 为了删除外面的数字,在每组结果中,最快和最慢的3%的时间被丢弃。 垃圾收集已经被请求,并且线程放弃并且在运行每个方法的调用之前睡觉。 JVM预热是在前20%的工作中完成的,这些结果被丢弃。

对我来说,原因是可能的performance,代码清晰度可以忽略不计。

请注意, fill方法的实现是微不足道的,一个简单的for循环设置每个数组元素为null。 所以,用实际的实现replace对它的调用不会明显降低调用方法的清晰度和简洁性。

如果考虑到所涉及的一切,潜在的绩效收益并不是很微不足道:

  1. JVM不需要parsingArrays类,如果需要的话加载并初始化它。 这是JVM执行几个步骤的一个不重要的过程。 首先,它检查类加载器,看看这个类是否已经加载,每当调用一个方法时,都会发生这种情况。 这里涉及到的优化当然有,但还是需要一些努力。 如果这个类没有被加载,那么JVM将需要经过昂贵的加载过程,validation字节码,解决其他必要的依赖关系,最后执行类的静态初始化(这可以是任意昂贵的)。 考虑到HashMap是这样一个核心类, Arrays是一个如此庞大的类(3600 +线),避免这些成本可能会加起来明显的节省。

  2. 由于没有Arrays.fill(...)方法调用,JVM将不必决定是否/何时将方法内联到调用者的主体中。 由于HashMap#clear()往往会被调用很多,所以JVM最终会执行内联,这需要JIT重新编译clear方法。 在没有方法调用的情况下, clear将始终以最高速度运行(一次初始化为JITed)。

Arrays不再调用方法的另一个好处是它简化了java.util包内的依赖关系图,因为一个依赖关系被删除了。

我要在黑暗中拍摄…

我的猜测是,它可能已经被改变,以为专业化 (又称基本typesgenerics)奠定基础。 也许也许我坚持),这个改变是为了让Java 10的转换更容易,在专业化成为JDK的一部分的情况下。

如果你看一下专业化的文件 , 语言限制部分的状态,它说:

因为任何typesvariables都可以采用值和引用types,所以涉及这种typesvariables的types检查规则(以下称为“avars”)。 例如,对于一个阿瓦尔T:

  • 无法将null转换为types为T的variables
  • 无法将T比较为空
  • 不能将T转换为对象
  • 无法将T []转换为对象[]

(强调是我的)。

然后在Specializer转换部分,它说:

当专门化任何generics类时,专家将执行大多数本地化的转换,但是一些需要类或方法的全局视图,其中包括:

  • typesvariablesreplace和名称修改在所有方法的签名上执行

稍后,在文件结尾附近的进一步调查部分,它说:

虽然我们的实验已经certificate这种专业化是实用的,但是还需要更多的调查。 具体而言,我们需要针对任何核心JDK库(特别是Collections和Streams)执行一些针对性的实验。


现在,关于变化…

如果Arrays.fill(Object[] array, Object value)方法将被专门化,那么它的签名应该改变成Arrays.fill(T[] array, T value) 。 但是,这种情况在(已经提到的) 语言限制部分中具体列出(这会违反强调的项目)。 所以也许有人认为最好不要从HashMap.clear()方法中使用它,特别是如果valuenull

在两个版本的循环之间没有实际的function差异。 Arrays.fill完成同样的事情。

所以使用它的select不一定被认为是一个错误。 这是由开发人员决定何时进行这种微观pipe理。

每种方法都有两个不同的问题:

  • 使用Arrays.fill使得代码更Arrays.fill ,更具可读性。
  • 直接在HashMap代码中循环(如版本8),明智的做法实际上是一个更好的select。 虽然插入Arrays类的开销可以忽略不计,但当涉及到像HashMap那样广泛的性能增强的每一点都有很大影响时(想象一下在fullblown webapp中最小的HashMap占用空间)。 考虑到Arrays类只用于这一个循环的事实。 这种变化足够小,不会使清晰的方法变得不可读。

如果没有问开发者究竟是谁做了这个确切的理由,但是我怀疑这是一个错误还是一个小小的提升。 更好的select。

我的意见是可以被认为是一个提高,即使只是偶然。