如果重复相同的代码,为什么 Java 更快?
Why is Java faster if it repeats the same code?
给定以下代码:
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次)
虚拟机的 JIT(即时)编译器优化了 Java 字节码的解释。例如,如果你有一个 if() 语句,它在大约 99% 的情况下都是错误的,jit 会针对错误情况优化你的代码,这最终会使你的真实情况变慢。抱歉英语不好。
你看到的是一些 JIT 优化的结果,现在看看你收到的所有评论,这一事实应该很清楚了。但真正发生的是什么以及为什么在外部 for
?
的相同数量的迭代之后几乎总是优化该代码
我会尝试回答这两个问题,但请记住,这里解释的所有内容都仅 与 Oracle 的 Hotspot VM 相关。没有 Java 规范来定义 JVM JIT 的行为方式。
首先,让我们看看 JIT 在做什么 运行使用一些额外的标志来测试程序(普通的 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)
每一列的含义如下:
- 时间戳
- 编译 ID(如果需要,还有其他属性)
- 分层编译级别
- 方法简称(如果可用,带有@
osr_bci
)
- 编译方法大小
仅保留与 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),这意味着getTimes
中包含的10k循环已被编译与其余部分隔离方法 并且 JVM 将方法代码的 部分 替换为已编译版本。 osr_bci
是指向这个新编译代码块的索引。
接下来的编译是经典的JIT编译,编译了所有的getTimes
方法(大小还是一样的,因为那个方法里面除了循环什么都没有)
第三次执行另一个 OSR,但在不同的层级。在 Java7 中添加了分层编译,基本上允许 JVM 选择 client 或 server JIT 模式 在运行时间,必要时在两者之间自由切换。客户端模式执行一组更简单的优化策略,而服务器模式能够应用更复杂的优化,另一方面在编译时间方面具有更大的成本。
我不会详细介绍不同模式或分层编译,如果您需要更多信息,我建议您 Java Performance: The Definitive Guide by Scott Oaks and also check this question 解释级别之间的变化。
回到 PrintCompilation 的输出,这里的要点是从某个时间点开始,执行一系列复杂度增加的编译,直到方法变得明显稳定(即 JIT 不再编译它) .
那么,为什么所有这一切都在主循环的 5-10 次迭代之后的某个时间点开始?
因为内部 getTimes
循环变成了 "hot".
Hotspot VM,通常定义"hot" 那些方法 至少被调用了 10k 次(这是历史默认阈值,可以使用 -XX:CompileThreshold=<num>
,通过分层编译,现在有多个阈值)但在 OSR 的情况下,我猜测它是在代码块被认为 "hot" 足够时执行的,就绝对或相对执行时间而言,在方法包含它。
其他参考文献
PrintCompilation Guide 作者:莫晶晶
示例:优化前的代码
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;
}
}
Originally, the code contained two calls to the b.get() method. After
optimization, the two method calls are optimized into a single
variable-copy operation; that is, the optimized code does not need to
perform a method call to acquire the field value of class B.
给定以下代码:
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次)
虚拟机的 JIT(即时)编译器优化了 Java 字节码的解释。例如,如果你有一个 if() 语句,它在大约 99% 的情况下都是错误的,jit 会针对错误情况优化你的代码,这最终会使你的真实情况变慢。抱歉英语不好。
你看到的是一些 JIT 优化的结果,现在看看你收到的所有评论,这一事实应该很清楚了。但真正发生的是什么以及为什么在外部 for
?
我会尝试回答这两个问题,但请记住,这里解释的所有内容都仅 与 Oracle 的 Hotspot VM 相关。没有 Java 规范来定义 JVM JIT 的行为方式。
首先,让我们看看 JIT 在做什么 运行使用一些额外的标志来测试程序(普通的 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)
每一列的含义如下:
- 时间戳
- 编译 ID(如果需要,还有其他属性)
- 分层编译级别
- 方法简称(如果可用,带有@
osr_bci
) - 编译方法大小
仅保留与 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),这意味着getTimes
中包含的10k循环已被编译与其余部分隔离方法 并且 JVM 将方法代码的 部分 替换为已编译版本。 osr_bci
是指向这个新编译代码块的索引。
接下来的编译是经典的JIT编译,编译了所有的getTimes
方法(大小还是一样的,因为那个方法里面除了循环什么都没有)
第三次执行另一个 OSR,但在不同的层级。在 Java7 中添加了分层编译,基本上允许 JVM 选择 client 或 server JIT 模式 在运行时间,必要时在两者之间自由切换。客户端模式执行一组更简单的优化策略,而服务器模式能够应用更复杂的优化,另一方面在编译时间方面具有更大的成本。
我不会详细介绍不同模式或分层编译,如果您需要更多信息,我建议您 Java Performance: The Definitive Guide by Scott Oaks and also check this question 解释级别之间的变化。
回到 PrintCompilation 的输出,这里的要点是从某个时间点开始,执行一系列复杂度增加的编译,直到方法变得明显稳定(即 JIT 不再编译它) .
那么,为什么所有这一切都在主循环的 5-10 次迭代之后的某个时间点开始?
因为内部 getTimes
循环变成了 "hot".
Hotspot VM,通常定义"hot" 那些方法 至少被调用了 10k 次(这是历史默认阈值,可以使用 -XX:CompileThreshold=<num>
,通过分层编译,现在有多个阈值)但在 OSR 的情况下,我猜测它是在代码块被认为 "hot" 足够时执行的,就绝对或相对执行时间而言,在方法包含它。
其他参考文献
PrintCompilation Guide 作者:莫晶晶
示例:优化前的代码
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;
}
}
Originally, the code contained two calls to the b.get() method. After optimization, the two method calls are optimized into a single variable-copy operation; that is, the optimized code does not need to perform a method call to acquire the field value of class B.