簡體   English   中英

為什么在循環中運行多個 lambda 會突然變慢?

[英]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 對我們有幫助,但我們遇到了哪個限制?

不會改變行為的事情:

  • 使用匿名內部類而不是 lambda,
  • 使用 3+ 個不同的嵌套類而不是 lambda。

“修復”行為的東西。 實際上,第三次調用和所有后續調用似乎要快得多,暗示編譯正確地完全消除了循環:

  • 使用 1-2 個嵌套類而不是 lambda,
  • 使用 1-2 個 lambda 實例而不是 4 個不同的實例,
  • 不在循環內調用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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM