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を使用しています。
完全な答えは、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);
}
私は@ 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の多くの仕事を引き起こすのかはまだ明らかではありません。