JVM常见概念之不怎么常见的一些陷阱

问题

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 编译器开销。另一方面,这会使预热变得复杂:为了避免突然重新编译,您需要使用与稍后运行的配置文件类似的配置文件进行预热,以便编译所有路径。

相关推荐
程序猿20234 小时前
MAT(memory analyzer tool)主要功能
jvm
期待のcode7 小时前
Java虚拟机的非堆内存
java·开发语言·jvm
jmxwzy11 小时前
JVM(java虚拟机)
jvm
Maỿbe11 小时前
JVM中的类加载&&Minor GC与Full GC
jvm
人道领域12 小时前
【零基础学java】(等待唤醒机制,线程池补充)
java·开发语言·jvm
小突突突12 小时前
浅谈JVM
jvm
饺子大魔王的男人14 小时前
远程调试总碰壁?局域网成 “绊脚石”?Remote JVM Debug与cpolar的合作让效率飙升
网络·jvm
天“码”行空1 天前
java面向对象的三大特性之一多态
java·开发语言·jvm
独自破碎E1 天前
JVM的内存区域是怎么划分的?
jvm
期待のcode1 天前
认识Java虚拟机
java·开发语言·jvm