我注意到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()
更好的解释。它更快吗?还是更安全?
答案 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
在应用程序代码之前加载Arrays
,HashMap
仅在HashMap
之后加载了14个类。 sun.reflect.Reflection
初始化会HashMap
加载,因为它有Arrays
个静态字段。 WeakHashMap
加载可能由Arrays.fill
加载触发,clear()
方法实际上WeakHashMap
加载java.lang.ClassValue$ClassValueMap
。 WeakHashMap
加载由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日replaced与HashMap
并且在讨论{{{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。因此,用实际实现替换对它的调用不会导致调用方法的清晰度/简洁性显着降低。
如果您考虑所涉及的一切,潜在的绩效优势并非如此微不足道:
JVM不需要解析Arrays
类,如果需要,还可以加载和初始化它。这是一个非平凡的过程,JVM执行几个步骤。首先,它检查类加载器以查看该类是否已经加载,并且每次调用方法时都会发生这种情况;当然,这里涉及到优化,但仍需要一些努力。如果没有加载类,JVM将需要经历昂贵的加载过程,验证字节码,解析其他必要的依赖关系,最后执行类的静态初始化(这可能是任意昂贵的)。鉴于HashMap
是一个核心类,并且Arrays
是如此庞大的类(3600多行),避免这些成本可能会显着节省成本。
由于没有Arrays.fill(...)
方法调用,JVM将不必决定是否/何时将方法内联到调用者的正文中。由于HashMap#clear()
往往被调用很多,JVM最终将执行内联,这需要JIT重新编译clear
方法。如果没有方法调用,clear
将始终以最高速度运行(一旦最初JITed)。
不再在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()
方法,尤其是当value
为null
时。
答案 4 :(得分:0)
2版本循环之间的功能没有实际差异。 Arrays.fill
完全相同。
因此,使用与否的选择未必被视为错误。由开发人员决定何时进行这种微观管理。
每种方法都有两个不同的问题:
Arrays.fill
会使代码更简洁,更易读。HashMap
代码中循环(如版本8),实际上是一个更好的选择。虽然插入Arrays
类的开销可以忽略不计,但是当涉及像HashMap
那样广泛的事情时,它可能会变得更少,其中每一点性能增强都会产生很大影响(想象一下,最小的足迹减少了完整的webapp中的HashMap)。考虑到Arrays类仅用于这一个循环的事实。这种变化足够小,不会使清晰方法的可读性降低。如果没有询问开发人员是否真的这样做,就无法找到确切的原因,但我怀疑这是一个错误或一个小的增强。 更好的选择。
我认为它可以被视为一种增强,即使只是偶然。