JIT - 微优化 - if 语句消除
JIT - micro optimization - if statement elimination
假设我们有以下代码:
public static void check() {
if (Config.initialized) {
...
}
}
Config.initialized 一开始是假的,只有在方法已经被 JIT 编译后的某个时候才变为真。该值永远不会返回到 false。
我 "know" 有很多非常复杂的优化正在进行(循环展开、分支预测、内联、逃逸分析等),尽管我离详细了解所有这些还很远我目前主要对以下内容感兴趣:
JIT 编译器是否有办法检测 if 在某个时间点后始终为真,以便可以完全跳过检查?我所说的完全是指没有变量访问、没有条件检查/jne 等...
如果 JIT 无法摆脱(从某个点开始)样本中不必要的检查(我不知道它是怎么做到的)有什么我可以做的做什么来支持?我唯一的想法是重新转换 class 并在初始化事件发生后从字节码中删除不必要的代码。
我知道这是全面的微优化,即使使用像 JMH 这样的工具也可能很难测量,但我仍然想知道和理解。
最后但同样重要的是:
- 我的理解是否正确,如果上述方法在某处内联,那么所有这些方法都将被重新编译(假设它们是热的)以防发生某些变化,从而需要重新编译
check
方法?
如果我正确理解了 JitWatch 测试的结果,那么上述问题的答案应该是:
- 不,不可能。总会有条件检查。
- 真的只有转型
- 是
- 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
.
- is there anything I could do to support that?
java.lang.invoke.MutableCallSite
救援。
此 class 专为完成您要求的事情而设计。它的 setTarget
方法支持在运行时重新绑定调用站点。在幕后,它会导致当前已编译方法的去优化,并可能在以后使用新目标重新编译它。
调用MutableCallSite
目标的MethodHandle
可以通过dynamicInvoker
方法获得。请注意 MethodHandle
应该是 static final
以允许内联。
- 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
假设我们有以下代码:
public static void check() {
if (Config.initialized) {
...
}
}
Config.initialized 一开始是假的,只有在方法已经被 JIT 编译后的某个时候才变为真。该值永远不会返回到 false。
我 "know" 有很多非常复杂的优化正在进行(循环展开、分支预测、内联、逃逸分析等),尽管我离详细了解所有这些还很远我目前主要对以下内容感兴趣:
JIT 编译器是否有办法检测 if 在某个时间点后始终为真,以便可以完全跳过检查?我所说的完全是指没有变量访问、没有条件检查/jne 等...
如果 JIT 无法摆脱(从某个点开始)样本中不必要的检查(我不知道它是怎么做到的)有什么我可以做的做什么来支持?我唯一的想法是重新转换 class 并在初始化事件发生后从字节码中删除不必要的代码。
我知道这是全面的微优化,即使使用像 JMH 这样的工具也可能很难测量,但我仍然想知道和理解。
最后但同样重要的是:
- 我的理解是否正确,如果上述方法在某处内联,那么所有这些方法都将被重新编译(假设它们是热的)以防发生某些变化,从而需要重新编译
check
方法?
如果我正确理解了 JitWatch 测试的结果,那么上述问题的答案应该是:
- 不,不可能。总会有条件检查。
- 真的只有转型
- 是
- 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
.
- is there anything I could do to support that?
java.lang.invoke.MutableCallSite
救援。
此 class 专为完成您要求的事情而设计。它的 setTarget
方法支持在运行时重新绑定调用站点。在幕后,它会导致当前已编译方法的去优化,并可能在以后使用新目标重新编译它。
调用MutableCallSite
目标的MethodHandle
可以通过dynamicInvoker
方法获得。请注意 MethodHandle
应该是 static final
以允许内联。
- 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