问题
JVM的优化器对程序可以利用的常量值做了什么工作?
基础知识
常量的优化是最便捷的,常量已经在编译时进行了处理,在应用运行时不需要做如何操作,那对于变量、常量变量、静态变量、常量静态变量编译和运行时会有什么区别呢?
class M {
final int x;
M(int x) { this.x = x; }
}
M m1 = new M(1337);
M m2 = new M(8080);
void work(M m) {
return m.x; // what to compile in here, 1337 or 8080?
}
实验
源码
在这里插入代码片@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class JustInTimeConstants {
static final long x_static_final = Long.getLong("divisor", 1000);
static long x_static = Long.getLong("divisor", 1000);
final long x_inst_final = Long.getLong("divisor", 1000);
long x_inst = Long.getLong("divisor", 1000);
@Benchmark public long _static_final() { return 1000 / x_static_final; }
@Benchmark public long _static() { return 1000 / x_static; }
@Benchmark public long _inst_final() { return 1000 / x_inst_final; }
@Benchmark public long _inst() { return 1000 / x_inst; }
}
运行结果
Benchmark Mode Cnt Score Error Units
JustInTimeConstants._inst avgt 15 9.670 ± 0.014 ns/op
JustInTimeConstants._inst_final avgt 15 9.690 ± 0.036 ns/op
JustInTimeConstants._static avgt 15 9.705 ± 0.015 ns/op
JustInTimeConstants._static_final avgt 15 1.899 ± 0.001 ns/op
通过-prof perfasm进行进一步分析,结果如下:
# JustInTimeConstants._inst / _inst_final hottest loop
0.21% ↗ mov 0x40(%rsp),%r10
0.02% │ mov 0x18(%r10),%r10 ; get field x_inst / x_inst_final
| ...
0.13% │ idiv %r10 ; ldiv
76.59% 95.38% │ mov 0x38(%rsp),%rsi ; prepare and consume the value (JMH infra)
0.40% │ mov %rax,%rdx
0.10% │ callq CONSUME
| ...
1.51% │ test %r11d,%r11d ; call @Benchmark again
╰ je BACK
如上述执行结果所示,大部分时间成本都花在执行实际的整数除法上。
# JustInTimeConstants._static hottest loop
0.04% ↗ movabs $0x7826385f0,%r10 ; native mirror for JustInTimeConstants.class
0.02% │ mov 0x70(%r10),%r10 ; get static x_static
| ...
0.02% │ idiv %r10 ;*ldiv
72.78% 95.51% | mov 0x38(%rsp),%rsi ; prepare and consume the value (JMH infra)
0.38% │ mov %rax,%rdx
0.04% 0.06% │ data16 xchg %ax,%ax
0.02% │ callq CONSUME
| ...
0.13% │ test %r11d,%r11d ; call @Benchmark again
╰ je BACK
如上述执行结果所示,static修饰的字段,它从静态字段所在的本地类镜像中读取静态字段。由于运行时知道我们正在处理的类(静态字段访问是静态解析的!),我们将常量指针内联到镜像,并通过其预定义的偏移量访问该字段。但是,由于我们不知道字段的值是什么——实际上有人可能在代码生成后更改了它——我们仍然执行相同的整数除法。
# JustInTimeConstants._static_final hottest loop
1.36% 1.40% ↗ mov %r8,(%rsp)
7.73% 7.40% │ mov 0x8(%rsp),%rdx ; <--- slot holding the "long" constant "1"
0.45% 0.51% │ mov 0x38(%rsp),%rsi ; prepare and consume the value (JMH infra)
3.59% 3.24% │ nop
1.44% 0.54% │ callq CONSUME
| ...
3.46% 2.37% │ test %r10d,%r10d ; call @Benchmark again
╰ je BACK
如上述_static_final的汇编执行结果,JIT 编译器确切地知道它正在处理的值,因此它可以积极地对其进行优化。在这里,循环计算只是重用了保存预先计算的值“1000 / 1000”的槽,即“1”。因此,性能可以通过编译器通过static final进行常量折叠的能力来解释。
总结
在上述示例中,字节码编译器(例如 javac)不知道static final字段的值是什么,因为该字段是用运行时值初始化的。当 JIT 编译发生时,类已成功初始化,并且值已存在,可以使用!这实际上是即时常量。这允许开发非常高效但运行时可调整的代码:事实上,整个示例被认为是基于预处理器的断言的替代品。而在C++领域,因为编译是完全提前的,因此如果你想让关键代码依赖于运行时选项。
这个示例的一个重要部分是解释器/分层编译。类初始化器通常是冷代码,因为它们只执行一次。但更重要的是处理类初始化的延迟部分,当我们想在第一次访问字段时加载和初始化类时。解释器或基线 JIT 编译器(例如 Hotspot 中的 C1)为我们运行它。当优化 JIT 编译器(例如 Hotspot 中的 C2)为相同方法运行时,重新编译的方法所需的类通常已完全初始化,并且它们的static final已完全已知。