当从未执行的代码被注释掉时,Java 程序运行速度较慢

2022-08-31 17:17:36

我在我的一个Java程序中观察到一些奇怪的行为。我试图尽可能地精简代码,同时仍然能够复制行为。代码全文如下。

public class StrangeBehaviour {

    static boolean recursionFlag = true;

    public static void main(String[] args) {
        long startTime = System.nanoTime();
        for (int i = 0; i < 10000; i ++) {
            functionA(6, 0);
        }
        long endTime = System.nanoTime();
        System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
    }

    static boolean functionA(int recursionDepth, int recursionSwitch) {
        if (recursionDepth == 0) { return true; }
        return functionB(recursionDepth, recursionSwitch);
    }

    static boolean functionB(int recursionDepth, int recursionSwitch) {
        for (int i = 0; i < 16; i++) {
            if (StrangeBehaviour.recursionFlag) {
                if (recursionSwitch == 0) {
                    if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
                } else {
                    if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
                }
            } else {
                // This block is never entered into.
                // Yet commenting out one of the lines below makes the program run slower!
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
            }
        }
        return false;
    }
}

我有两个函数,它们以递归方式相互调用。这两个函数都采用一个控制递归终止的参数。 调用次数最多为一次,且未更改。 使用 调用 16 次。当使用 a 调用时,递归终止。functionA()functionB()recursionDepthfunctionA()functionB()recursionDepthfunctionB()functionA()recursionDepth - 1functionA()recursionDepth0

functionB()具有包含多个调用的代码块。此块永远不会输入到中,因为输入由一个变量控制,该变量设置为在程序执行期间永远不会更改。但是,即使注释掉其中一个调用也会导致程序运行速度变慢。在我的计算机上,执行时间为<0.2s,所有调用都存在,当其中一个调用被注释掉时,执行时间为>2s。System.out.println()boolean recursionFlagtrueprintln()println()

是什么导致了这种行为?我唯一的猜测是,有一些幼稚的编译器优化是由与代码块的长度(或函数调用次数等)相关的参数触发的。任何对此的进一步见解将不胜感激!

编辑:我使用的是JDK 1.8。


答案 1

注释的代码会影响内联的处理方式。如果函数 B 变得更长/变大(更多的字节码指令),它将不会内联到函数 A 中。

因此@J3D1能够使用VMOptions手动关闭functionB()的内联:这似乎消除了较短函数的延迟。-XX:CompileCommand=dontinline,com.jd.benchmarking.StrangeBeh‌​aviour::functionB

使用 vm 选项,您可以显示内联-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

更大的版本,不会内联功能B

@ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 21   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 35   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large

较短的版本将尝试内联函数B,从而导致一些进一步的尝试。

@ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
 @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
 @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 21   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
@ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
@ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
    @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep

主要是猜测,但较大/内联的字节码会导致分支预测和缓存问题


答案 2

完整的答案是k5_和托尼的答案的结合。

OP发布的代码省略了在执行基准测试之前触发HotSpot编译的预热循环;因此,当包含 print 语句时,速度提高了 10 倍(在我的计算机上),它结合了在 HotSpot 中将字节码编译为 CPU 指令所花费的时间,以及 CPU 指令的实际运行。

如果我在定时循环之前添加一个单独的预热循环,则 print 语句的加速速度只有 2.5 倍。

这表明,当方法内联时,HotSpot/JIT 编译需要更长的时间(正如 Tony 所解释的那样),以及代码的运行需要更长的时间,可能是因为缓存或分支预测/流水线性能较差,如 k5_ 所示。

public static void main(String[] args) {
    // Added the following warmup loop before the timing loop
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }

    long startTime = System.nanoTime();
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }
    long endTime = System.nanoTime();
    System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}

推荐