Jak*_*kes 4 java performance jit jmh
考虑以下代码:
public class Playground {
private static final int MAX = 100_000_000;
public static void main(String... args) {
execute(() -> {});
execute(() -> {});
execute(() -> {});
execute(() -> {});
}
public static void execute(Runnable task) {
Stopwatch stopwatch = Stopwatch.createStarted();
for (int i = 0; i < MAX; i++) {
task.run();
}
System.out.println(stopwatch);
}
}
Run Code Online (Sandbox Code Playgroud)
目前,我在 Temurin 17 上的 Intel MBP 上打印以下内容:
3.675 ms
1.948 ms
216.9 ms
243.3 ms
Run Code Online (Sandbox Code Playgroud)
请注意,第三次(以及任何后续)执行时速度减慢了 100*。现在,显然,这不是用Java 编写基准测试的方法。循环代码不执行任何操作,因此我希望它能够消除所有重复。另外,我无法使用 JMH 重复这种效果,这告诉我原因很棘手且脆弱。
那么,为什么会发生这种情况呢?为什么会突然出现如此灾难性的减速,幕后到底发生了什么?一个假设是 C2 对我们进行保释,但我们遇到了哪些限制?
不改变行为的事情:
“修正”行为的事情。实际上,第三次调用和所有后续调用似乎要快得多,这表明编译正确地完全消除了循环:
task.run()不在循环内调用lambda,execute()方法,仍然维护 4 个不同的 lambda。您实际上可以使用 JMH SingleShot 模式复制此内容:
@BenchmarkMode(Mode.SingleShotTime)
@Warmup(iterations = 0)
@Measurement(iterations = 1)
@Fork(1)
public class Lambdas {
@Benchmark
public static void doOne() {
execute(() -> {});
}
@Benchmark
public static void doFour() {
execute(() -> {});
execute(() -> {});
execute(() -> {});
execute(() -> {});
}
public static void execute(Runnable task) {
for (int i = 0; i < 100_000_000; i++) {
task.run();
}
}
}
Run Code Online (Sandbox Code Playgroud)
Benchmark Mode Cnt Score Error Units
Lambdas.doFour ss 0.446 s/op
Lambdas.doOne ss 0.006 s/op
Run Code Online (Sandbox Code Playgroud)
如果你查看测试-prof perfasm配置文件doFour,你会得到一条重要线索:
....[Hottest Methods (after inlining)]..............................................................
32.19% c2, level 4 org.openjdk.Lambdas$$Lambda$44.0x0000000800c258b8::run, version 664
26.16% c2, level 4 org.openjdk.Lambdas$$Lambda$43.0x0000000800c25698::run, version 658
Run Code Online (Sandbox Code Playgroud)
至少有两个热 lambda,它们由不同的类表示。因此,您看到的可能是单态(一个目标),然后是双态(两个目标),然后是多态虚拟调用task.run。
虚拟调用必须选择从哪个类调用实现。类越多,优化器的情况就越糟糕。JVM 试图适应,但随着运行的进行,情况变得越来越糟。大致是这样的:
execute(() -> {}); // compiles with single target, fast
execute(() -> {}); // recompiles with two targets, a bit slower
execute(() -> {}); // recompiles with three targets, slow
execute(() -> {}); // continues to be slow
Run Code Online (Sandbox Code Playgroud)
现在,消除循环需要看透task.run(). 在单态和双态情况下,这很容易:一个或两个目标被内联,它们的空主体被发现并完成。在这两种情况下,您都必须进行类型检查,这意味着它不是完全免费的,双态的成本有点额外。当你经历多态调用时,就完全没有这样的运气了:它是不透明的调用。
您可以在组合中添加另外两个基准测试来查看它:
@Benchmark
public static void doFour_Same() {
Runnable l = () -> {};
execute(l);
execute(l);
execute(l);
execute(l);
}
@Benchmark
public static void doFour_Pair() {
Runnable l1 = () -> {};
Runnable l2 = () -> {};
execute(l1);
execute(l1);
execute(l2);
execute(l2);
}
Run Code Online (Sandbox Code Playgroud)
然后会产生:
Benchmark Mode Cnt Score Error Units
Lambdas.doFour ss 0.445 s/op ; polymorphic
Lambdas.doFour_Pair ss 0.016 s/op ; bimorphic
Lambdas.doFour_Same ss 0.008 s/op ; monomorphic
Lambdas.doOne ss 0.006 s/op
Run Code Online (Sandbox Code Playgroud)
这也解释了为什么你的“修复”有帮助:
使用 1-2 个嵌套类而不是 lambda,
双态内联。
Run Code Online (Sandbox Code Playgroud)using 1-2 lambda instances instead of 4 different ones,
双态内联。
Run Code Online (Sandbox Code Playgroud)not calling task.run() lambdas inside the loop,
避免循环中的多态(不透明)调用,允许循环消除。
Run Code Online (Sandbox Code Playgroud)inlining the execute() method, still maintaining 4 different lambdas.
避免单个呼叫站点经历多个呼叫目标。换句话说,将单个多态调用站点转换为一系列单态调用站点,每个调用站点都有自己的目标。