[英]Why does running multiple lambdas in loops suddenly slow down?
考虑以下代码:
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);
}
}
这目前在 Temurin 17 上的 Intel MBP 上打印以下内容:
3.675 ms
1.948 ms
216.9 ms
243.3 ms
请注意第三次(以及任何后续)执行的 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();
}
}
}
Benchmark Mode Cnt Score Error Units
Lambdas.doFour ss 0.446 s/op
Lambdas.doOne ss 0.006 s/op
如果您查看doFour
测试的-prof perfasm
配置文件,您会得到一个重要的线索:
....[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
至少有两个热 lambda,它们由不同的类表示。 所以你看到的可能是单态的(一个目标),然后是双态的(两个目标),然后是task.run
的多态虚拟调用。
虚拟调用必须选择从哪个class调用实现。 你拥有的类越多,优化器就越糟糕。 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
现在,消除循环需要看穿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);
}
然后会产生:
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
这也解释了为什么您的“修复”有帮助:
使用 1-2 个嵌套类而不是 lambda,
双态内联。
using 1-2 lambda instances instead of 4 different ones,
双态内联。
not calling task.run() lambdas inside the loop,
避免循环中的多态(不透明)调用,允许循环消除。
inlining the execute() method, still maintaining 4 different lambdas.
避免遇到多个呼叫目标的单个呼叫站点。 换句话说,将单个多态调用站点变成一系列单态调用站点,每个调用站点都有自己的目标。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.