JIT - 微优化 - if 语句消除

JIT - micro optimization - if statement elimination

假设我们有以下代码:

public static void check() {   
    if (Config.initialized) {
           ...
    }
}

Config.initialized 一开始是假的,只有在方法已经被 JIT 编译后的某个时候才变为真。该值永远不会返回到 false。

我 "know" 有很多非常复杂的优化正在进行(循环展开、分支预测、内联、逃逸分析等),尽管我离详细了解所有这些还很远我目前主要对以下内容感兴趣:

  1. JIT 编译器是否有办法检测 if 在某个时间点后始终为真,以便可以完全跳过检查?我所说的完全是指没有变量访问、没有条件检查/jne 等...

  2. 如果 JIT 无法摆脱(从某个点开始)样本中不必要的检查(我不知道它是怎么做到的)有什么我可以做的做什么来支持?我唯一的想法是重新转换 class 并在初始化事件发生后从字节码中删除不必要的代码。

我知道这是全面的微优化,即使使用像 JMH 这样的工具也可能很难测量,但我仍然想知道和理解。

最后但同样重要的是:

  1. 我的理解是否正确,如果上述方法在某处内联,那么所有这些方法都将被重新编译(假设它们是热的)以防发生某些变化,从而需要重新编译 check 方法?

如果我正确理解了 JitWatch 测试的结果,那么上述问题的答案应该是:

  1. 不,不可能。总会有条件检查。
  2. 真的只有转型
  1. Does the JIT compiler have a way to detect that the if will always be true after a certain point

是的,如果该字段是 static final,并且它的持有者 class 在 JIT 编译器启动时已经初始化。显然这不适用于您的情况,因为 Config.initialized 无法制作 static final.

  1. is there anything I could do to support that?

java.lang.invoke.MutableCallSite 救援。

此 class 专为完成您要求的事情而设计。它的 setTarget 方法支持在运行时重新绑定调用站点。在幕后,它会导致当前已编译方法的去优化,并可能在以后使用新目标重新编译它。

调用MutableCallSite目标的MethodHandle可以通过dynamicInvoker方法获得。请注意 MethodHandle 应该是 static final 以允许内联。

  1. if the above method got inlined somewhere that all those methods would be recompiled

是的。

这是一个基准测试,表明 mutableCallSite 方法在开始时与 alwaysFalse 一样快,并且在切换切换后也与 alwaysTrue 一样快。正如@Holger 建议的那样,我还包括一个静态字段切换以供比较。

package bench;

import org.openjdk.jmh.annotations.*;
import java.lang.invoke.*;
import java.util.concurrent.*;

@State(Scope.Benchmark)
public class Toggle {
    static boolean toggleField = false;

    static final MutableCallSite toggleCallSite =
            new MutableCallSite(MethodHandles.constant(boolean.class, false));

    static final MethodHandle toggleMH = toggleCallSite.dynamicInvoker();

    public void switchToggle() {
        toggleField = true;
        toggleCallSite.setTarget(MethodHandles.constant(boolean.class, true));
        MutableCallSite.syncAll(new MutableCallSite[]{toggleCallSite});
        System.out.print("*** Toggle switched *** ");
    }

    @Setup
    public void init() {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
        executor.schedule(this::switchToggle, 10100, TimeUnit.MILLISECONDS);
        executor.shutdown();
    }

    @Benchmark
    public int alwaysFalse() {
        return 0;
    }

    @Benchmark
    public int alwaysTrue() {
        return ThreadLocalRandom.current().nextInt();
    }

    @Benchmark
    public int field() {
        if (toggleField) {
            return ThreadLocalRandom.current().nextInt();
        } else {
            return 0;
        }
    }

    @Benchmark
    public int mutableCallSite() throws Throwable {
        if ((boolean) toggleMH.invokeExact()) {
            return ThreadLocalRandom.current().nextInt();
        } else {
            return 0;
        }
    }
}

运行 具有 5 次预热迭代和 10 次测量迭代的基准我得到以下结果:

# JMH version: 1.20
# VM version: JDK 1.8.0_192, VM 25.192-b12

# Benchmark: bench.Toggle.alwaysFalse

# Run progress: 0,00% complete, ETA 00:01:00
# Fork: 1 of 1
# Warmup Iteration   1: 3,875 ns/op
# Warmup Iteration   2: 3,369 ns/op
# Warmup Iteration   3: 2,699 ns/op
# Warmup Iteration   4: 2,696 ns/op
# Warmup Iteration   5: 2,703 ns/op
Iteration   1: 2,697 ns/op
Iteration   2: 2,696 ns/op
Iteration   3: 2,696 ns/op
Iteration   4: 2,706 ns/op
Iteration   5: *** Toggle switched *** 2,698 ns/op
Iteration   6: 2,698 ns/op
Iteration   7: 2,692 ns/op
Iteration   8: 2,707 ns/op
Iteration   9: 2,712 ns/op
Iteration  10: 2,702 ns/op


# Benchmark: bench.Toggle.alwaysTrue

# Run progress: 25,00% complete, ETA 00:00:48
# Fork: 1 of 1
# Warmup Iteration   1: 5,159 ns/op
# Warmup Iteration   2: 5,198 ns/op
# Warmup Iteration   3: 4,314 ns/op
# Warmup Iteration   4: 4,321 ns/op
# Warmup Iteration   5: 4,306 ns/op
Iteration   1: 4,306 ns/op
Iteration   2: 4,310 ns/op
Iteration   3: 4,297 ns/op
Iteration   4: 4,324 ns/op
Iteration   5: *** Toggle switched *** 4,356 ns/op
Iteration   6: 4,300 ns/op
Iteration   7: 4,310 ns/op
Iteration   8: 4,290 ns/op
Iteration   9: 4,297 ns/op
Iteration  10: 4,294 ns/op


# Benchmark: bench.Toggle.field

# Run progress: 50,00% complete, ETA 00:00:32
# Fork: 1 of 1
# Warmup Iteration   1: 3,596 ns/op
# Warmup Iteration   2: 3,429 ns/op
# Warmup Iteration   3: 2,973 ns/op
# Warmup Iteration   4: 2,937 ns/op
# Warmup Iteration   5: 2,934 ns/op
Iteration   1: 2,927 ns/op
Iteration   2: 2,928 ns/op
Iteration   3: 2,932 ns/op
Iteration   4: 2,929 ns/op
Iteration   5: *** Toggle switched *** 3,002 ns/op
Iteration   6: 4,887 ns/op
Iteration   7: 4,866 ns/op
Iteration   8: 4,877 ns/op
Iteration   9: 4,867 ns/op
Iteration  10: 4,877 ns/op


# Benchmark: bench.Toggle.mutableCallSite

# Run progress: 75,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 3,474 ns/op
# Warmup Iteration   2: 3,332 ns/op
# Warmup Iteration   3: 2,750 ns/op
# Warmup Iteration   4: 2,701 ns/op
# Warmup Iteration   5: 2,701 ns/op
Iteration   1: 2,697 ns/op
Iteration   2: 2,696 ns/op
Iteration   3: 2,699 ns/op
Iteration   4: 2,706 ns/op
Iteration   5: *** Toggle switched *** 2,771 ns/op
Iteration   6: 4,310 ns/op
Iteration   7: 4,306 ns/op
Iteration   8: 4,312 ns/op
Iteration   9: 4,317 ns/op
Iteration  10: 4,301 ns/op