Java 为什么这个内循环比通过外循环的第一次迭代快4倍?

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

我试图重现中描述的一些处理器缓存效果。我知道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 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