问题
JIT 编译的最佳部分是什么?如果 JIT 决定编译该方法,它会编译其中的所有内容吗?我是否应该使用真实数据来预热方法?JIT 编译器有哪些技巧可以优化其编译时间?
基础知识
JIT编译器针对方法进行工作:一旦某个方法被视为热代码,运行时系统就会要求 JIT编译器生成该方法的优化版本。因此,JIT 会编译整个方法并将其交给运行时系统。
但事实是,允许推测编译/去优化的运行时系统允许JIT使用关于其行为的一系列假设来编译方法。我们之前在隐式空值检查中见过它。这次,我们将研究有关冷代码的更常见的一些东西。
考虑一下只有使用 flag = true 才能有效调用的这个方法:
java
void m(boolean flag) {
if (flag) {
// do stuff A
} else {
// do stuff B
}
}
即使从分析中不知道 flag ,智能 JIT 编译器也可以使用分支分析来确定"B"分支永远不会被采用,并将其编译为:
java
void m() {
if (condition) {
// do stuff A
} else {
// Assume this branch is never taken.
<trap to runtime system: uncommon branch is taken>
}
}
因此,实际上从未编译过分支 B 中的实际代码。通过避免处理永远不需要的代码,这节省了编译时间,通常还提高了代码密度。
请注意,这与基于分支频率的代码布局不同。在这种情况下,当其中一个分支频率恰好为零时,我们可以完全跳过编译其主体。当且仅当采用该分支时,生成的代码才会陷入运行时系统,表明违反了编译先决条件,并且 JIT 将在新条件下重新生成方法主体,这次编译现在并不罕见的分支。
实验
源码
java
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ColdCodeBench {
@Param({"onlyA", "onlyB", "swap"})
String test;
boolean condition;
@Setup(Level.Iteration)
public void setup() {
switch (test) {
case "onlyA":
condition = true;
break;
case "onlyB":
condition = false;
break;
case "swap":
condition = !condition;
break;
}
}
int v = 1;
int a = 1;
int b = 1;
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void test() {
if (condition) {
v *= a;
} else {
v *= b;
}
}
}
在这个测试中,我们要么只采用分支 A,要么只采用分支 B,或者我们在每次迭代时在它们之间进行切换。
此测试的目的是以简单的方式演示生成的代码。 在这个简单的测试中, 所有版本的性能大致相同。实际上,冷分支可能需要大量代码,尤其是在内联之后,并且对编译时间和生成的代码密度的性能影响将是巨大的。
毫不奇怪,与 "基于频率的代码布局" 一致,我们可以看到"onlyA"和"onlyB"测试都立即布局了第一个分支。但随后,发生了一件奇怪的事情:根本没有第二个分支代码!相反,有一个所谓的"不常见陷阱"的调用!这是对运行时的通知,我们已不满足编译条件,现在采用这个"不常见"分支。
bash
# "onlyA"
9.54% ...3cc: movzbl 0x18(%rsi),%r10d ; load and test $condition
0.21% ...3d1: test %r10d,%r10d
╭ ...3d4: je ...3f6
│ ; if true, then...
0.90% │ ...3d6: mov 0x10(%rsi),%r10d ; ...load $a...
7.81% │ ...3da: imul 0xc(%rsi),%r10d ; ...multiply by $v...
17.33% │ ...3df: mov %r10d,0xc(%rsi) ; ...store to $v...
8.16% │ ...3e3: add $0x20,%rsp ; ...and return.
0.60% │ ...3e7: pop %rbp
0.18% │ ...3e8: cmp 0x340(%r15),%rsp
0.02% │ ...3ef: ja ...408
10.51% │ ...3f5: retq
│ ; if false, then...
↘ ...3f6: mov %rsi,%rbp
...3f9: mov %r10d,(%rsp)
...3fd: mov $0xffffff45,%esi
...402: nop
...403: callq <runtime> ; - (reexecute) o.o.CCB::test@4 (line 73)
; {runtime_call UncommonTrapBlob}
# "onlyB"
10.21% ...acc: movzbl 0x18(%rsi),%r10d ; load and test $condition
0.25% ...ad1: test %r10d,%r10d
╭ ...ad4: jne ...af6
│ ; if false, then...
0.29% │ ...ad6: mov 0x14(%rsi),%r10d ; ...load $b...
8.78% │ ...ada: imul 0xc(%rsi),%r10d ; ...multiply by $v...
18.87% │ ...adf: mov %r10d,0xc(%rsi) ; ...store $v...
9.74% │ ...ae3: add $0x20,%rsp ; ...and return.
0.24% │ ...ae7: pop %rbp
0.27% │ ...ae8: cmp 0x340(%r15),%rsp
│ ...aef: ja ...b08
9.76% │ ...af5: retq
│ ; if true, then...
↘ ...af6: mov %rsi,%rbp
...af9: mov %r10d,(%rsp)
...afd: mov $0xffffff45,%esi
...b02: nop
...b03: callq <runtime> ; - (reexecute) o.o.CCB::test@4 (line 73)
; {runtime_call UncommonTrapBlob}
当最终采用该"冷"分支时,JVM 将重新编译该方法。它将在 -XX:+PrintCompilation 日志中显示如下:
bash
# Warmup Iteration 1: ...
// Profiled version is compiled with C1 (+MDO)
351 476 3 org.openjdk.ColdCodeBench::test (37 bytes)
// C2 version is installed
352 477 4 org.openjdk.ColdCodeBench::test (37 bytes)
// Profiled version is declared dead
352 476 3 org.openjdk.ColdCodeBench::test (37 bytes) made not entrant
# Warmup Iteration 2: ...
// Deopt! C2 version is declared dead
1361 477 4 org.openjdk.ColdCodeBench::test (37 bytes) made not entrant
// Re-profiling version is compiled with C1 (+counters)
1363 498 2 org.openjdk.ColdCodeBench::test (37 bytes)
// New C2 version is installed
1364 499 4 org.openjdk.ColdCodeBench::test (37 bytes)
// Re-profiling version is declared dead
1364 498 2 org.openjdk.ColdCodeBench::test (37 bytes) made not entrant
在"swap"情况下,最终结果清晰可见。两个分支都经过编译:
bash
4.25% ...f2c: mov 0xc(%rsi),%r11d ; load $v
6.23% ...f30: movzbl 0x18(%rsi),%r10d ; load and test $condition
0.04% ...f35: test %r10d,%r10d
╭ ...f38: je ...f45
│ ; if false, then
0.02% │ ...f3a: imul 0x10(%rsi),%r11d ; ...multiply by $a...
13.33% │ ...f3f: mov %r11d,0xc(%rsi) ; ...store $v
3.82% │╭ ...f43: jmp ...f4e
││ ; if true, then
0.02% ↘│ ...f45: imul 0x14(%rsi),%r11d ; ...multiply by $b...
18.70% │ ...f4a: mov %r11d,0xc(%rsi) ; ...store $v
6.12% ↘ ...f4e: add $0x10,%rsp
...f52: pop %rbp
0.08% ...f53: cmp 0x340(%r15),%rsp
...f5a: ja ...f61
10.81% ...f60: retq
总结
高级JIT 编译器可以只编译方法的实际活动部分。这简化了生成的代码和 JIT 编译器开销。另一方面,这会使预热变得复杂:为了避免突然重新编译,您需要使用与稍后运行的配置文件类似的配置文件进行预热,以便编译所有路径。