当永远不会执行的代码被注释掉时,Java程序会运行得更慢

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (148)

我在其中一个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;
    }
}

我有两个函数,functionA()并且functionB()递归地调用对方。两个函数都有一个recursionDepth控制递归终止的参数。最多functionA()调用functionB()一次,recursionDepth保持不变。functionB()打电话functionA()16次recursionDepth - 1。递归终止时,functionA()被称为用recursionDepth0

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

什么可能导致这种行为?我唯一的猜测是,有一些天真的编译器优化是由与代码块长度有关的参数(或函数调用次数等)触发的。任何进一步的洞察将非常感谢!

我正在使用JDK 1.8。

提问于
用户回答回答于

完整答案是k5_和Tony答案的组合。

在发布基准之前,OP发布的代码省略了热启动循环以触发热点编译; 因此,当打印语句被包含时,加倍10倍(在我的计算机上)加速了HotSpot花费在CPU指令上的字节码编译时间,以及CPU指令的实际运行。

如果在定时循环之前添加单独的预热循环,则打印语句的加速只有2.5倍。

这表明,当内联方法(如Tony解释的)时,HotSpot / JIT编译需要更长的时间,并且代码的运行需要更长的时间,可能是因为缓存或分支预测/流水线性能较差,正如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);
}
用户回答回答于

评论代码影响如何处理内联。如果函数B获得更长/更大(更多的字节码指令),它将不会被内联到函数A中。

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

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

@ 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

较短的版本将尝试内联functionB,导致进一步的尝试。

@ 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

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

扫码关注云+社区