如果重复使用相同的代码,为什么Java会更快?

Ald*_*uno 11 java optimization jit jvm jvm-hotspot

给出以下代码:

public class Test{

    static int[] big = new int [10000];

    public static void main(String[] args){
        long time;
        for (int i = 0; i < 16; i++){
            time = System.nanoTime();
            getTimes();
            System.out.println(System.nanoTime() - time);
        }    
    }
    public static void getTimes(){
        int d;
        for (int i = 0; i < 10000; i++){
            d = big[i];
        }    
    }
}
Run Code Online (Sandbox Code Playgroud)

输出显示持续时间递减趋势:

171918
167213
165930
165502
164647
165075
203991
70563
45759
43193
45759
44476
45759
52601
47897
48325
Run Code Online (Sandbox Code Playgroud)

为什么getTimes在执行8次或更多次后,在不到三分之一的时间内执行相同的代码?(编辑:它并不总是发生在第8次,但从5日到10日)

Umb*_*ndi 10

你看到的是一些JIT优化的结果,现在应该清楚地看到你收到的所有评论.但是真正发生了什么以及为什么代码优化总是几乎在外部迭代次数相同之后for

我将尝试回答这两个问题,但请记住,此处说明的所有内容与Oracle的Hotspot VM相关.没有Java规范定义JVM JIT应该如何表现.

首先,让我们看看JIT正在使用一些额外的标志来运行Test程序(普通的JVM足以运行它,不需要加载调试共享库,某些 UnlockDiagnosticVMOptions选项需要):

java -XX:+PrintCompilation Test
Run Code Online (Sandbox Code Playgroud)

执行完成后使用此输出(在开头删除几行,表明正在编译其他方法):

[...]
195017
184573
184342
184262
183491
189494
    131   51%      3       Test::getTimes @ 2 (22 bytes)
245167
    132   52       3       Test::getTimes (22 bytes)
165144  

65090
    132   53       1       java.nio.Buffer::limit (5 bytes)
59427
    132   54%      4       Test::getTimes @ 2 (22 bytes)  
75137
48110    
    135   51%     3        Test::getTimes @ -2 (22 bytes)   made not entrant

    142   55       4       Test::getTimes (22 bytes)
150820
86951
90012
91421
Run Code Online (Sandbox Code Playgroud)

printlns从你的代码交错与相关的JIT正在执行编译诊断信息.看一行:

131    51%      3       Test::getTimes @ 2 (22 bytes)
Run Code Online (Sandbox Code Playgroud)

每列具有以下含义:

  1. 时间戳
  2. 编译标识(如果需要,还有其他属性)
  3. 分层编译级别
  4. 方法短名称(带@,osr_bci如果可用)
  5. 编译方法大小

仅保留与以下内容相关的行getTimes:

    131   51%      3       Test::getTimes @ 2 (22 bytes)
    132   52       3       Test::getTimes (22 bytes)
    132   54%      4       Test::getTimes @ 2 (22 bytes)     
    135   51%      3       Test::getTimes @ -2 (22 bytes)   made not entrant
    142   55       4       Test::getTimes (22 bytes)
Run Code Online (Sandbox Code Playgroud)

很明显,getTimes不止一次编译,但每次编译都以不同的方式编译.

%符号意味着已经执行了堆栈替换(OSR),这意味着包含在其中的10k循环getTimes已经与方法的其余部分隔离编译,并且JVM用编译版本替换了方法代码的该部分.这osr_bci是一个指向这个新编译的代码块的索引.

下一个编译是一个经典的JIT编译,它编译所有getTimes方法(大小仍然相同,因为除了循环之外,该方法中没有其他内容).

第三次执行另一个OSR但处于不同的分层级别.Java7中添加了分层编译,基本上允许JVM 在运行时选择客户端服务器 JIT模式,必要时可以在两者之间自由切换.客户端模式执行一组更简单的优化策略,而服务器模式能够应用更复杂的优化,另一方面,在编译时花费更大的成本.

我不会详细介绍不同的模式或分层编译,如果您需要其他信息,我建议使用Scott Performance: Scott Oaks 的权威指南,并检查这个问题,解释各级之间的变化.

回到PrintCompilation的输出,这里的要点是,从某个时间点开始,执行一系列复杂度增加的编译,直到方法变得明显稳定(即JIT不再编译它).

那么,为什么所有这些都是在主循环的5-10次迭代后的某个特定时间点开始的?

因为内环getTimes变得"热".

该HotSpot虚拟机,通常定义的"热"这些方法已经被调用至少10,000次(这是历史的默认阈值,可以使用变更-XX:CompileThreshold=<num>,与分层编译现在有多个阈值),但在OSR的情况下,我猜当一块代码被认为足够"热"时,就绝对或相对执行时间而言,在方法内部包含它时执行它.

其他参考文献

Krystal Mok的PrintCompilation Guide

Java性能:权威指南