Java 为什么这个内循环比通过外循环的第一次迭代快4倍?
我试图重现中描述的一些处理器缓存效果。我知道Java是一个托管环境,这些示例无法准确翻译,但我遇到了一个奇怪的情况,我尝试将其提炼为一个简单的示例来说明其效果:Java 为什么这个内循环比通过外循环的第一次迭代快4倍?,java,performance,jit,Java,Performance,Jit,我试图重现中描述的一些处理器缓存效果。我知道Java是一个托管环境,这些示例无法准确翻译,但我遇到了一个奇怪的情况,我尝试将其提炼为一个简单的示例来说明其效果: public static void main(String[] args) { final int runs = 10; final int steps = 1024 * 1024 * 1024; for (int run = 0; run < runs; run++) { final
public static void main(String[] args) {
final int runs = 10;
final int steps = 1024 * 1024 * 1024;
for (int run = 0; run < runs; run++) {
final int[] a = new int[1];
long start = System.nanoTime();
for (int i = 0; i < steps; i++) {
a[0]++;
}
long stop = System.nanoTime();
long time = TimeUnit.MILLISECONDS.convert(stop - start, TimeUnit.NANOSECONDS);
System.out.printf("Time for loop# %2d: %5d ms\n", run, time);
}
}
内部循环的第一次迭代大约是后续迭代的4倍。这与我通常期望的相反,因为通常随着JIT的开始,性能会上升
当然,在任何严肃的微基准测试中,人们都会进行几个预热循环,但我很好奇是什么导致了这种行为,特别是如果我们知道循环可以在24毫秒内执行,那么稳态时间超过100毫秒就不是很令人满意了
对于我正在使用的JDK(在linux上),请参考:
更新:
以下是一些基于一些评论和一些实验的更新信息:
1) 将System.out I/O移出循环(通过将计时存储在大小为“运行”的数组中)不会在时间上产生显著差异
2) 上面显示的输出是在Eclipse中运行时显示的。当我从命令行编译和运行(使用相同的JDK/JVM)时,我得到的结果比较温和,但仍然非常显著(速度提高了2倍而不是4倍)。这似乎很有趣,因为如果有任何问题的话,我们完全在eclipse中运行会减慢速度
3) 将a
向上移动,使其脱离循环,以便在每次迭代中重复使用,不会产生任何效果
4) 如果将int[]a
更改为long[]a
,则第一次迭代的运行速度更快(约20%),而其他迭代的运行速度仍然相同(较慢)
更新2:
我想阿潘金的答案可以解释这一点。我在Sun的1.9 JVM上尝试了这一点,它是从:
openjdk version "1.8.0_40"
OpenJDK Runtime Environment (build 1.8.0_40-b20)
OpenJDK 64-Bit Server VM (build 25.40-b23, mixed mode)
Time for loop# 0: 48 ms
Time for loop# 1: 116 ms
Time for loop# 2: 112 ms
Time for loop# 3: 113 ms
Time for loop# 4: 112 ms
Time for loop# 5: 112 ms
Time for loop# 6: 111 ms
Time for loop# 7: 111 ms
Time for loop# 8: 113 ms
Time for loop# 9: 113 ms
致:
这是相当大的进步 这是方法的次优重新编译 JIT编译器依赖于在解释期间收集的运行时统计数据。当第一次编译
main
方法时,外部循环尚未完成其第一次迭代=>运行时统计数据表明内部循环之后的代码从未执行过,因此JIT不会费心编译它。它产生了一个不寻常的陷阱
当内部循环第一次结束时,不常见的陷阱被命中,导致方法被去优化
在外循环的第二次迭代中,main
方法用新知识重新编译。现在JIT有更多的统计信息和更多的上下文需要编译。出于某种原因,现在它不会在寄存器中缓存值a[0]
(可能是因为JIT被更广泛的上下文愚弄了)。因此,它生成addl
指令来更新内存中的数组,这实际上是内存加载和存储的组合
相反,在第一次编译期间,JIT将a[0]
的值缓存在寄存器中,只有mov
指令将值存储在内存中(无负载)
快速循环(第一次迭代):
不是ms-bs,字节秒:PI可以在我的MacBook上报告稍微不那么引人注目(2.2x)但类似的结果:
java版本“1.8.0_40”java(TM)SE运行时环境(构建1.8.0_40-b27)java热点(TM)64位服务器VM(构建25.40-b25,混合模式)
ha,是的,我想纳秒应该是1000。真的应该是time=TimeUnit.millides.convert(停止-启动,TimeUnit.NANOSECONDS);我无法在Windows 7运行时确认此行为:java版本“1.7.0_45”java(TM)SE运行时环境(构建1.7.0_45-b18)java热点(TM)64位服务器VM(构建24.45-b08,混合模式)
。时间大约为60毫秒。我无法在运行的MBP Retina上确认这一行为:java版本“1.8.0_25”java(TM)SE运行时环境(build 1.8.0_25-b17)java HotSpot(TM)64位服务器VM(build 25.25-b02,混合模式)
您是如何提取编译的代码的?您是否使用了调试虚拟机?@Turing85-XX:+UnlockDiagnosticVMOptions-XX:+PrintAssembly
感谢您的调查。我试图解析整个程序集,但我不清楚您注释为“register/memory中的increment”的行,看起来[0]正递增16($0x10)。这是某种优化吗?我无法复制那个输出——我得到了一个简单的‘inc’(尽管我可以看到,在重新编译之后,‘inc’将进入内存,而不是寄存器,正如您所描述的)。问题中代码的输出是否正确?@JamesScriven对,由于循环展开优化,[0]一次递增16。为了将编译限制为只使用有趣的方法,我添加了-XX:CompileOnly=.main-XX:-tieredcomilation
openjdk version "1.8.0_40"
OpenJDK Runtime Environment (build 1.8.0_40-b20)
OpenJDK 64-Bit Server VM (build 25.40-b23, mixed mode)
openjdk version "1.8.0_40"
OpenJDK Runtime Environment (build 1.8.0_40-b20)
OpenJDK 64-Bit Server VM (build 25.40-b23, mixed mode)
Time for loop# 0: 48 ms
Time for loop# 1: 116 ms
Time for loop# 2: 112 ms
Time for loop# 3: 113 ms
Time for loop# 4: 112 ms
Time for loop# 5: 112 ms
Time for loop# 6: 111 ms
Time for loop# 7: 111 ms
Time for loop# 8: 113 ms
Time for loop# 9: 113 ms
java version "1.9.0-ea"
Java(TM) SE Runtime Environment (build 1.9.0-ea-b73)
Java HotSpot(TM) 64-Bit Server VM (build 1.9.0-ea-b73, mixed mode)
Time for loop# 0: 48 ms
Time for loop# 1: 26 ms
Time for loop# 2: 22 ms
Time for loop# 3: 22 ms
Time for loop# 4: 22 ms
Time for loop# 5: 22 ms
Time for loop# 6: 22 ms
Time for loop# 7: 22 ms
Time for loop# 8: 22 ms
Time for loop# 9: 23 ms
0x00000000029fc562: mov %ecx,0x10(%r14) <<< array store
0x00000000029fc566: mov %r11d,%edi
0x00000000029fc569: mov %r9d,%ecx
0x00000000029fc56c: add %edi,%ecx
0x00000000029fc56e: mov %ecx,%r11d
0x00000000029fc571: add $0x10,%r11d <<< increment in register
0x00000000029fc575: mov %r11d,0x10(%r14) <<< array store
0x00000000029fc579: add $0x11,%ecx
0x00000000029fc57c: mov %edi,%r11d
0x00000000029fc57f: add $0x10,%r11d
0x00000000029fc583: cmp $0x3ffffff2,%r11d
0x00000000029fc58a: jl 0x00000000029fc562
0x00000000029fa1b0: addl $0x10,0x10(%r14) <<< increment in memory
0x00000000029fa1b5: add $0x10,%r13d
0x00000000029fa1b9: cmp $0x3ffffff1,%r13d
0x00000000029fa1c0: jl 0x00000000029fa1b0
Time for loop# 0: 104 ms
Time for loop# 1: 101 ms
Time for loop# 2: 91 ms
Time for loop# 3: 63 ms
Time for loop# 4: 60 ms
Time for loop# 5: 60 ms
Time for loop# 6: 59 ms
Time for loop# 7: 55 ms
Time for loop# 8: 57 ms
Time for loop# 9: 59 ms