为什么Arrays.fill()不再用于HashMap.clear()了?

时间:2015-09-21 11:09:37

标签: java arrays hashmap java-8

我注意到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对于空映射可以为null,因此需要在局部变量中进​​行额外的检查和缓存。但为什么Arrays.fill()被for-loop取代?

似乎这一变化是在this commit中引入的。不幸的是,我没有找到为什么plain for循环可能比Arrays.fill()更好的解释。它更快吗?还是更安全?

5 个答案:

答案 0 :(得分:29)

我将尝试总结三个在评论中提出的更合理的版本。

@Holger says

  

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

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

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仅在HashMap之后加载了14个类。 sun.reflect.Reflection初始化会HashMap加载,因为它有Arrays个静态字段。 WeakHashMap加载可能由Arrays.fill加载触发,clear()方法实际上WeakHashMap加载java.lang.ClassValue$ClassValueMapWeakHashMap加载由ClassValueMap触发,扩展java.lang.Class。每个Arrays实例中都存在Arrays。所以对我来说似乎没有java.lang.Throwable类,JDK根本无法初始化。 java.util.Arrays静态初始值设定项也很短,它只初始化断言机制。此机制在许多其他类中使用(例如,包括很早加载的WeakHashMap.clear())。在Arrays.fill中没有执行其他静态初始化步骤。因此@Holger版本对我来说似乎不正确。

在这里我们也发现了非常有趣的事情。 fill仍然使用tab。当它出现在那里时很有意思,但不幸的是,它会转到prehistoric times(它已经存在于第一个公共OpenJDK存储库中)。

接下来,@ MarcoTopolnik says

  

肯定不是更安全,但如果fill来电没有内联且Arrays.fill很短,那么可能会更快。在HotSpot上,循环和显式MaxInlineLevel调用将导致快速编译器内在(在快乐的一天场景中)。

我真的很惊讶,Arrays.fill并非直接内化(请参阅intrinsic list生成的@apangin)。似乎JVM可以识别和矢量化这样的循环,而无需明确的内部处理。因此,在特定情况下(例如,如果达到WeakHashMap.clear()限制),不能内联额外调用,这是正确的。另一方面,它是非常罕见的情况,它只是一个呼叫,它不是内部呼叫,它是静态的,而不是虚拟/接口呼叫,因此性能改进可能只是微不足道的,仅在某些特定情况下。不是JVM开发人员通常关心的事情。

还应该注意的是,即使是C1&#39;客户&#39;编译器(层1-3)能够内联-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) 中调用,因为内联日志(HashMap.clear())说:

Arrays.fill

当然,它也可以通过智能且功能强大的C2服务器轻松实现。编译器。因此,我认为这里没有问题。似乎@Marco版本也不正确。

最后我们从@StuartMarks获得了一些comments(他们是JDK开发人员,因此是一些官方声音):

  

有趣。我的预感是这是一个错误。此更改集的审核主题为here,并引用earlier thread continued here。早期线程中的初始消息指向Doug Lea的CVS存储库中的HashMap.java原型。我不知道这是从哪里来的。它似乎与OpenJDK历史中的任何内容都不匹配。

     

......无论如何,它可能是一些旧的快照; for循环多年来一直在clear()方法中。 Arrays.fill()调用是由this changeset引入的,所以它只在树上持续了几个月。另请注意,基于this changeset引入的Integer.highestOneBit()的二次幂计算也同时消失,尽管这已被注意到但在审核期间被驳回。嗯。

确实HashMap包含了多年的循环,在{2013年4月10日replacedHashMap并且在讨论{{{4}时保持不到一半半3}}被介绍了。讨论的提交实际上是修复commit问题HashMap内部的重大改写。这是一个很长的故事,关于使用具有重复哈希码的密钥来毒害HashMap的可能性,将Arrays.fill搜索速度降低到线性,使其容易受到DoS攻击。解决此问题的尝试在JDK-7中执行,包括String hashCode的一些随机化。所以似乎diff -U0实现是从先前的提交中分离出来的,独立开发,然后合并到主分支中,覆盖介于其间的几个更改。

我们可能支持执行差异的这个假设。取Arrays.fill移除diff -U0的位置(2013-09-04),并将其与JDK-8023463(2013-07-30)进行比较。 Arrays.fill输出有4341行。现在让我们在添加Arrays.fill之前对version进行区分(2013-04-01)。现在WeakHashMap只包含2680行。因此,较新的版本实际上更像旧版本而不是直接版本。

<强>结论

总而言之,我同意Stuart Marks的看法。没有具体的理由删除Arrays,这只是因为错误地覆盖了中间的更改。在JDK代码和用户应用程序中使用Arrays.fill完全没问题,例如在{{1}}中使用。在JDK初始化期间很早就加载{{1}}类,非常简单的静态初始化器和{{1}}方法甚至可以通过客户端编译器轻松内联,因此不应该注意性能缺陷。

答案 1 :(得分:3)

因为它很多更快!

我对这两种方法的缩减版本进行了一些彻底的基准测试:

std::vector

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

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% 是1个标准差,表示为结果的百分比(fyi,“正态分布”群体的SD为68),(sd)是JDK 8相对于JDK 7的时间安排。

有趣的是,它不仅显着更快,而且偏差也稍微窄一些,这意味着JDK 8实现提供了更多一致的性能。

在填充了随机vs对象的阵列上,测试在jdk 1.8.0_45上运行了数百(数百)次。为了删除外部数字,在每组结果上丢弃最快和最慢3%的时间。请求垃圾收集,并且在运行方法的每次调用之前,线程都会产生并睡眠。在前20%的工作中完成了JVM热身,这些结果被丢弃了。

答案 2 :(得分:1)

对我来说,原因可能是表现在改善,在代码清晰度方面成本可以忽略不计。

请注意,fill方法的实现很简单,一个简单的for循环将每个数组元素设置为null。因此,用实际实现替换对它的调用不会导致调用方法的清晰度/简洁性显着降低。

如果您考虑所涉及的一切,潜在的绩效优势并非如此微不足道:

  1. JVM不需要解析Arrays类,如果需要,还可以加载和初始化它。这是一个非平凡的过程,JVM执行几个步骤。首先,它检查类加载器以查看该类是否已经加载,并且每次调用方法时都会发生这种情况;当然,这里涉及到优化,但仍需要一些努力。如果没有加载类,JVM将需要经历昂贵的加载过程,验证字节码,解析其他必要的依赖关系,最后执行类的静态初始化(这可能是任意昂贵的)。鉴于HashMap是一个核心类,并且Arrays是如此庞大的类(3600多行),避免这些成本可能会显着节省成本。

  2. 由于没有Arrays.fill(...)方法调用,JVM将不必决定是否/何时将方法内联到调用者的正文中。由于HashMap#clear()往往被调用很多,JVM最终将执行内联,这需要JIT重新编译clear方法。如果没有方法调用,clear将始终以最高速度运行(一旦最初JITed)。

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

答案 3 :(得分:1)

我会在黑暗中拍摄......

我的猜测是它可能已被更改,以便为Specialization(也就是原始类型的泛型)奠定基础。 也许(我坚持也许),这种变化意味着在专业化成为JDK的一部分的情况下,更容易过渡到Java 10。

如果查看State of the Specialization document语言限制部分,则会显示以下内容:

  

因为任何类型变量都可以采用值以及引用类型,所以类型检查规则涉及这些类型变量(以下称为&#34; avars&#34;)。例如,对于avar T:

     
      
  • 无法将null转换为类型为T
  • 的变量   
  • 无法将T与null比较
  •   
  • 无法将T转换为对象
  •   
  • 无法将T []转换为对象[]
  •   
  • ...
  •   

(重点是我的)。

专业化转换部分中,它说:

  

当专门化任何泛型类时,专门化程序将执行大量本地化的转换,但有些转换需要全局视图的类或方法,包括:

     
      
  • ...
  •   
  • 在所有方法的签名上执行类型变量替换和名称修改
  •   
  • ...
  •   

稍后,在文档末尾,在进一步调查部分,它说:

  

虽然我们的实验证明以这种方式专业化是切实可行的,但还需要进行更多的调查。具体来说,我们需要针对任何正在运行的核心JDK库(特别是集合和流)执行大量针对性实验。

现在,关于改变......

如果Arrays.fill(Object[] array, Object value)方法将被专门化,则其签名应更改为Arrays.fill(T[] array, T value)。但是,这种情况具体列在(已经提到的)语言限制部分(它会违反强调的项目)。所以也许某人决定最好不要使用HashMap.clear()方法,尤其是当valuenull时。

答案 4 :(得分:0)

2版本循环之间的功能没有实际差异。 Arrays.fill完全相同。

因此,使用与否的选择未必被视为错误。由开发人员决定何时进行这种微观管理。

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

  • 使用Arrays.fill会使代码更简洁,更易读。
  • 直接在HashMap代码中循环(如版本8),实际上是一个更好的选择。虽然插入Arrays类的开销可以忽略不计,但是当涉及像HashMap那样广泛的事情时,它可能会变得更少,其中每一点性能增强都会产生很大影响(想象一下,最小的足迹减少了完整的webapp中的HashMap)。考虑到Arrays类仅用于这一个循环的事实。这种变化足够小,不会使清晰方法的可读性降低。

如果没有询问开发人员是否真的这样做,就无法找到确切的原因,但我怀疑这是一个错误或一个小的增强。 更好的选择。

我认为它可以被视为一种增强,即使只是偶然。