为什么在循环中运行多个 lambda 会突然变慢?

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 对我们进行保释,但我们遇到了哪些限制?

不改变行为的事情:

  • 使用匿名内部类而不是 lambda,
  • 使用 3 个以上不同的嵌套类而不是 lambda。

“修正”行为的事情。实际上,第三次调用和所有后续调用似乎要快得多,这表明编译正确地完全消除了循环:

  • 使用 1-2 个嵌套类而不是 lambda,
  • 使用 1-2 个 lambda 实例而不是 4 个不同的实例,
  • task.run()不在循环内调用lambda,
  • 内联该execute()方法,仍然维护 4 个不同的 lambda。

Ale*_*lev 9

您实际上可以使用 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,

双态内联。

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.
Run Code Online (Sandbox Code Playgroud)

避免单个呼叫站点经历多个呼叫目标。换句话说,将单个多态调用站点转换为一系列单态调用站点,每个调用站点都有自己的目标。