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


给出以下代码:

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];
        }    
    }
}

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

171918
167213
165930
165502
164647
165075
203991
70563
45759
43193
45759
44476
45759
52601
47897
48325

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


3303
2018-06-15 11:35


起源

你能不断重现这种行为? - CodeNewbie
看起来对我来说是JIT。一个非常尖锐的演示,为什么你不应该自己对Java进行基准测试,而是使用适当的微基准工具,如 江铃控股。 - Boris the Spider
为什么不在第一次或第二次迭代中删除它,而是在第8次? - MarkusK96
@VirajNalawade为什么第二?为什么不是10'000?因为JIT会优化使用频繁的代码 更多。如果我运行两次循环,那么死代码分析可能比运行它更昂贵。一旦我运行了一些阈值次数,JIT将开始优化代码。在某种程度上,该阈值是可配置的。 - Boris the Spider
与仅解释代码相比,JIT编译器将编译经常使用的代码以提供加速。它发生在第5次和第10次迭代之间可能只取决于编译需要的时间,即编译版本何时可用。 - Thomas


答案:


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

我会尝试回答这两个问题,但请记住,这里解释的一切都是相对的 只要 到Oracle的Hotspot VM。没有Java规范定义JVM JIT应该如何表现。

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

java -XX:+PrintCompilation Test

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

[...]
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

printlns 从您的代码中交叉出现与JIT正在执行的编译相关的诊断信息。 看一行:

131    51%      3       Test::getTimes @ 2 (22 bytes)

每列具有以下含义:

  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)

很明显 getTimes 正在被编译不止一次,但每次都以不同的方式编译。

% 符号表示已执行堆栈内替换(OSR),这意味着包含的10k循环 getTimes 已编译 与方法的其余部分隔离 并取代了JVM 那一节 带有编译版本的方法代码。该 osr_bci 是一个指向这个新编译的代码块的索引。

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

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

如果您需要我推荐的其他信息,我不会详细介绍不同模式或分层编译 Java性能:权威指南 由斯科特奥克斯和检查 这个问题 这解释了各级之间的变化。

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

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

因为内心 getTimes 循环变得“热”。

Hotspot VM通常定义“热”那些 方法 已被调用至少10k次(这是历史默认阈值,可以使用更改 -XX:CompileThreshold=<num>,使用分层编译,现在有多个阈值)但是在OSR的情况下,我猜测它是在代码块被认为足够“热”时执行的,在绝对或相对执行时间方面,在方法内部包含它。

其他参考文献

PrintCompilation指南 作者:Krystal Mok

Java性能:权威指南


9
2018-06-15 21:31



我的第二个建议是,Java性能:TPG确实非常宝贵,无论你是否真的需要从应用程序中挤出更多性能,或者你只是多管闲事。 - biziclop


虚拟机的JIT(即时)编译器优化了Java字节代码的解释。例如,如果你有一个if()语句,在大约99%的情况下是假的,那么jit会优化你的代码以获得错误的情况,这会使你的真实案例最终变慢。抱歉英语不好。


4
2018-06-15 11:43



你能提供一些参考吗? - Viraj Nalawade
en.wikipedia.org/wiki/Just-in-time_compilation - Stephen C
通过Bill Venners阅读Java 2虚拟机内部。虽然java版本很旧,但它会给你很好的JVM内部工作思路。 - User27854


示例:优化前的代码

class A {
  B b;
  public void newMethod() {
    y = b.get();  //calling get() function
    ...do stuff...
    z = b.get();   // calling again
    sum = y + z;
  }
}
class B {
   int value;
   final int get() {
      return value;
   }
}

示例:优化后的代码

class A {
B b;
public void newMethod() {
   y = b.value;
   ...do stuff...
   sum = y + y; 
}
}
class B {
   int value;
   final int get() {
      return value;
   }
}

最初,代码包含对b.get()方法的两次调用。后   优化, 两个方法调用被优化为一个   变量复制操作;也就是说,优化的代码不需要   执行方法调用以获取B类的字段值

阅读更多


0
2018-06-16 04:52