当从未执行的代码被注释掉时,Java程序运行得更慢

时间:2016-12-31 18:21:19

标签: java recursion compiler-optimization

我在一个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()使用recursionDepth - 1拨打functionA() 16次。使用recursionDepth 0调用functionB()时,递归会终止。

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

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

编辑:我使用的是JDK 1.8。

3 个答案:

答案 0 :(得分:41)

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

  

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

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

更大的版本,不会内联functionB

@ 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

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

答案 1 :(得分:21)

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

OP发布的代码在执行基准测试之前省略了一个热启动循环以触发HotSpot编译;因此,当包含打印语句时,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);
}

答案 2 :(得分:18)

我和@ k5_一起,似乎存在确定是否内联函数的阈值。如果JIT编译器决定内联它,那将导致大量工作和时间,SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3()); var dbName = Path.Combine(Path.GetTempPath(), "StackOverflow.db"); using (sqlite3 dbRaw = ugly.open(dbName)) { dbRaw.create_function("distance", 4, null, UDFDistanceFunction); double currentLatitude = 47.0; double currentLongitude = -122.0; var sql = $"SELECT * FROM barlocations WHERE distance('{currentLatitude.ToString()}', '{currentLongitude.ToString()}', barlocations.lat, barlocations.lng) <= 100 ;"; var locs = dbRaw.query<BarLocations>(sql); foreach (var loc in locs) { Console.WriteLine(loc.name); } } 显示:

 $scope.uploader.onCompleteItem = function (fileItem, response, status, headers) {
        console.info('onCompleteItem', fileItem, response, status, headers);
        console.info(response.name); // here I can get the response url
         $scope.imgurl = response.name; 
};

上面是没有注释的信息,以下是一个注释,它将方法大小从326字节减少到318字节。您可以注意到输出的第1列中的任务ID,后者中的任务ID更大,这会导致更多时间。

-XX:+PrintCompilation

如果您将代码更改为以下内容(添加两行并将打印行换行),您可以看到代码大小更改为326字节,并且现在运行得更快:

  task-id
    158   32       3       so_test.StrangeBehaviour::functionB (326 bytes)   made not entrant
    159   35       3       java.lang.String::<init> (82 bytes)
    160   36  s    1       java.util.Vector::size (5 bytes)
    1878   37 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    1898   38       3       so_test.StrangeBehaviour::main (65 bytes)
    2665   39       3       java.util.regex.Pattern::has (15 bytes)
    2667   40       3       sun.misc.FDBigInteger::mult (64 bytes)
    2668   41       3       sun.misc.FDBigInteger::<init> (30 bytes)
    2668   42       3       sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    2.51 seconds elapsed.

新时间和JIT编译器信息:

  task-id
    126   35       4       so_test.StrangeBehaviour::functionA (12 bytes)
    130   33       3       so_test.StrangeBehaviour::functionA (12 bytes)   made not entrant
    131   36  s    1       java.util.Vector::size (5 bytes)
    14078   37 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    14296   38       3       so_test.StrangeBehaviour::main (65 bytes)
    14296   39 %     4       so_test.StrangeBehaviour::functionB @ 2 (318 bytes)
    14300   40       4       so_test.StrangeBehaviour::functionB (318 bytes)
    14304   34       3       so_test.StrangeBehaviour::functionB (318 bytes)   made not entrant
    14628   41       3       java.util.regex.Pattern::has (15 bytes)
    14631   42       3       sun.misc.FDBigInteger::mult (64 bytes)
    14632   43       3       sun.misc.FDBigInteger::<init> (30 bytes)
    14632   44       3       sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    14.50 seconds elapsed.

总结

  • 当方法大小超过某些限制时,JIT不会内联此功能;
  • 如果我们注释掉一条线,它减小到一个低于阈值的尺寸,JIT决定内联它;
  • 内联该函数会导致许多JIT任务,这会减慢程序的速度。

<强>更新

收到my latest trial,这个问题的答案并不那么容易:

正如我的代码示例所示,正常的内联优化将

  • 加速计划
  • 并且不需要花费太多编译器工作(在我的测试中,当内联发生时它甚至花费更少的工作)。

但是在这个问题中,代码会导致很多JIT工作并减慢程序,这似乎是JIT的一个bug。目前还不清楚为什么会导致JIT如此多的工作。