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

相关推荐
江沉晚呤时8 分钟前
精益架构设计:深入理解与实践 C# 中的单一职责原则
java·jvm·算法·log4j·.netcore·net
剑海风云12 小时前
JVM常见概念之条件移动
jvm·条件移动
黄名富12 小时前
深入探究 JVM 堆的垃圾回收机制(一)— 判活
java·jvm
ling__wx12 小时前
JVM常见面试总结
java·jvm
重生成为码农‍20 小时前
类加载机制
java·开发语言·jvm
黄名富20 小时前
深入探究 JVM 堆的垃圾回收机制(二)— 回收
java·jvm·算法·系统架构
江沉晚呤时21 小时前
深入解析 C# 中的装饰器模式(Decorator Pattern)
java·开发语言·javascript·jvm·microsoft·.netcore
越甲八千1 天前
C++关键字汇总
jvm·c++
日暮南城故里1 天前
Java学习------初识JVM体系结构
java·jvm·学习