web-dev-qa-db-ja.com

Java実行されないコードがコメント化されると、プログラムの実行が遅くなります

Javaプログラムの1つで奇妙な振る舞いを観察しました。振る舞いを複製できるようにしながら、可能な限りコードを削除しようとしました。以下のコード全体で。

_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()の2つの関数があり、これらは互いに再帰的に呼び出します。両方の関数は、再帰の終了を制御するrecursionDepthパラメーターを取ります。 functionA()は、recursionDepthを変更せずにfunctionB()を最大1回呼び出します。 functionB()は、functionA()を_recursionDepth - 1_で16回呼び出します。 functionA()が_0_のrecursionDepthで呼び出されると、再帰は終了します。

functionB()には、多数のSystem.out.println()呼び出しを含むコードブロックがあります。エントリはtrueに設定された_boolean recursionFlag_変数によって制御され、プログラムの実行中に変更されることはないため、このブロックは入力されません。ただし、println()呼び出しの1つでもコメントアウトすると、プログラムの実行が遅くなります。私のマシンでは、すべてのprintln()呼び出しが存在する場合の実行時間は<0.2秒であり、呼び出しの1つがコメント化されている場合は> 2秒です。

この動作の原因は何ですか?私の唯一の推測は、コードブロックの長さ(または関数呼び出しの回数など)に関連するパラメーターによってトリガーされている素朴なコンパイラーの最適化があるということです。これに関するさらなる洞察は大歓迎です!

編集:私はJDK 1.8を使用しています。

57
J3D1

完全な答えは、k5_とトニーの答えの組み合わせです。

OPが投稿したコードは、ベンチマークを行う前にHotSpotコンパイルをトリガーするウォームアップループを省略します。したがって、printステートメントを含めると(コンピューター上で)10倍の高速化により、HotSpotでバイトコードをCPU命令にコンパイルするのに費やした時間と、CPU命令の実際の実行の両方が組み合わされます。

タイミングループの前に別のウォームアップループを追加すると、printステートメントの速度は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);
}
21
Erwin Bolwidt

私は@ k5_と一緒にいますが、関数をインライン化するかどうかを決定するしきい値が存在するようです。また、JITコンパイラーがインライン化することを決定した場合、-XX:+PrintCompilationショー:

  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.

上部はコメントなしの情報であり、以下はメソッドのサイズを326バイトから318バイトに減らすコメント付きです。そして、出力の列1のタスクIDは、後者では非常に大きく、より多くの時間がかかることに気付くことができます。

  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.

また、コードを次のように変更すると(2行追加して印刷行を出力)、コードサイズが326バイトに変更され、より高速に実行されることがわかります。

        if (StrangeBehaviour.recursionFlag) {
            int a = 1;
            int b = 1;
            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("...");
        }

新しい時間とJITコンパイラー情報:

    140   34       3       so_test.StrangeBehaviour::functionB (326 bytes)   made not entrant
    145   36       3       Java.lang.String::<init> (82 bytes)
    148   37  s    1       Java.util.Vector::size (5 bytes)
    162   38       4       so_test.StrangeBehaviour::functionA (12 bytes)
    163   33       3       so_test.StrangeBehaviour::functionA (12 bytes)   made not entrant
    1916   39 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    1936   40       3       so_test.StrangeBehaviour::main (65 bytes)
    2686   41       3       Java.util.regex.Pattern::has (15 bytes)
    2689   42       3       Sun.misc.FDBigInteger::mult (64 bytes)
    2690   43       3       Sun.misc.FDBigInteger::<init> (30 bytes)
    2690   44       3       Sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    2.55 seconds elapsed.

結論

  • メソッドのサイズが制限を超えている場合、JITはこの関数をインライン化しません。
  • そして、しきい値を下回るサイズに縮小する行をコメントアウトすると、JITはそれをインライン化することを決定します。
  • その関数をインライン化すると、プログラムを遅くする多くのJITタスクが発生します。

更新

私の最新の試用版 に慣れると、この質問に対する答えはそれほど簡単ではありません:

私のコードサンプルが示すように、通常のインライン最適化は

  • プログラムを加速する
  • コンパイラーの作業に多くの費用はかかりません(私のテストでは、インラインが発生した場合の作業の費用が少なくなります)。

しかし、この問題では、コードが多くのJIT作業を引き起こし、JITのバグと思われるプログラムの速度が低下します。そして、なぜそれがJITの多くの仕事を引き起こすのかはまだ明らかではありません。

18
Tony