JIT - 微优化 - 消除if语句

3

假设我们有以下代码:

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

在开始时,Config.initialized的值为false,只有在方法已经被JIT编译后的某个时间点才会更改为true。该值永远不会回到false。

我“知道”有许多非常复杂的优化正在进行(循环展开、分支预测、内联、逃逸分析等),尽管我远远不了解它们的细节,但现在我主要感兴趣的是以下内容:

  1. JIT编译器是否有一种方法可以检测到if语句在某个时间点之后将始终为真,以便完全跳过检查?完全跳过指的是没有变量访问、没有条件检查/jne等...

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

我知道这是完全微观的优化,可能甚至使用JMH等工具也很难衡量,但我仍然想知道和理解。

最后但并非最不重要的:

  1. 如果上述方法在某个地方被内联,那么只要发生变化以使check方法需要重新编译,所有这些方法都将被重新编译(假设它们是热点)吗?

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

  1. 没有,没有办法。始终会进行条件检查。
  2. 真正只能通过转换来实现。
  3. 是的

initializedvolatile的吗? - Iłya Bursov
请展示 Config 的声明。 - chrylis -cautiouslyoptimistic-
@chrylis:我认为这也应该回答了你的请求,不是吗?还有什么其他因素可能会影响情况呢? - Haasip Satang
@HaasipSatang 读写易失变量不会被编译器优化掉。 - Iłya Bursov
注意:您的CPU可以使用分支预测动态地消除代码,这对于紧密循环非常有效。例如,微基准测试。 - Peter Lawrey
显示剩余2条评论
1个回答

6
  1. JIT编译器是否有办法在某个时间点后检测到if语句将始终为true?

是的,如果字段是static final,并且它的持有类在JIT编译器启动时已被初始化。显然,在您的情况下不适用,因为Config.initialized无法设置为static final

  1. 我能做些什么来支持这个吗?

java.lang.invoke.MutableCallSite可以帮忙。

这个类专门设计用于您的需求。它的setTarget方法支持在运行时重新绑定调用站点。在幕后,它会导致当前编译方法的去优化,并可能稍后重新编译它以使用新的目标。

可以使用dynamicInvoker方法获取用于调用MutableCallSite目标的MethodHandle。请注意,MethodHandle应该是static final,以允许内联。

  1. 如果上述方法被内联到某个地方,则所有这些方法都将被重新编译

是的。

这里有一个基准测试,演示了mutableCallSite方法在开头与alwaysFalse一样快,切换后也与alwaysTrue一样快。我还包括了一个静态字段toggle进行比较,如@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

太棒了,谢谢!我刚刚验证了汇编代码确实是一模一样的。重新转换的想法也应该可行,不是吗?或者你认为这可能会导致任何不良影响吗? - Haasip Satang
@HaasipSatang 重新转换也可以,但我发现它不太方便,因为它需要加载代理才能访问Instrumentation API。 - apangin
谢谢,同意。上述方法更好、更简单,并且可以适用于我的情况(我已经使用了它)。不过代理已经到位了,并且与上面的代码一起调用了该方法。再次感谢。 - Haasip Satang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接