私は、2.7 GHz Intel Core i7を搭載したラップトップ上で次のJavaコードを実行しています。 2 ^ 32回の繰り返しでループを終了するのにかかる時間を測定することを目的としていました。これは、おおよそ1.48秒(4/2.7 = 1.48)と予想されました。
しかし、実際には1.48秒ではなく2ミリ秒しかかかりません。これがその下のJVM最適化の結果であるかどうか私は思っていますか?
public static void main(String[] args)
{
long start = System.nanoTime();
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++){
}
long finish = System.nanoTime();
long d = (finish - start) / 1000000;
System.out.println("Used " + d);
}
ここでは2つの可能性があります。
コンパイラは、ループが冗長で何もしていないことに気付いたため、ループを最適化しました。
JIT(ジャストインタイムコンパイラ)は、ループが冗長で何もしないことに気付いたため、最適化しました。
現代のコンパイラは非常に知的です。彼らはコードが役に立たなくなったときにそれを見ることができます。空のループを GodBolt に入れて出力を確認し、次に-O2
の最適化をオンにすると、出力は次のようになります。
main():
xor eax, eax
ret
私は何かを明確にしたいのですが、Javaではほとんどの最適化はJITによって行われます。他の言語(C/C++など)では、ほとんどの最適化は最初のコンパイラーによって行われます。
JITコンパイラによって最適化されたようです。オフにすると(-Djava.compiler=NONE
)、コードの実行速度が大幅に低下します。
$ javac MyClass.Java
$ Java MyClass
Used 4
$ Java -Djava.compiler=NONE MyClass
Used 40409
私はOPのコードをclass MyClass
の中に入れます。
明白なことを述べるつもりです - これが起こるJVM最適化であること、ループは単に全く取り除かれるでしょう。これはC1 Compiler
に対してのみ有効/有効にし、無効にした場合の巨大違いJIT
が何を意味するのかを示す小さなテストです。
免責事項:このようなテストを書かないでください - これは実際のループの「除去」がC2 Compiler
で起こることを証明するためだけのものです:
@Benchmark
@Fork(1)
public void full() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
++result;
}
}
@Benchmark
@Fork(1)
public void minusOne() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-XX:TieredStopAtLevel=1" })
public void withoutC2() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-Xint" })
public void withoutAll() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
結果はJIT
のどの部分が有効になっているかによって、メソッドが速くなることを示しています( "何もしていないようです - ループ除去、これは最大レベルであるC2 Compiler
で起こっているようです):
Benchmark Mode Cnt Score Error Units
Loop.full avgt 2 ≈ 10⁻⁷ ms/op
Loop.minusOne avgt 2 ≈ 10⁻⁶ ms/op
Loop.withoutAll avgt 2 51782.751 ms/op
Loop.withoutC2 avgt 2 1699.137 ms/op
すでに指摘したように、JIT(ジャストインタイム)コンパイラは、不要な繰り返しを削除するために空のループを最適化できます。しかし、どうですか?
実際には、JITコンパイラは2つあります。C1&C2。まず、コードをC1でコンパイルします。 C1は統計を収集し、100%のケースでは空のループが何も変わらず無駄であることをJVMが発見するのを助けます。この状況では、C2がステージに入ります。コードが頻繁に呼び出されるときは、収集された統計を使用してC2で最適化しコンパイルすることができます。
一例として、次のコードスニペットをテストします(私のJDKはslowdebug build 9-internalに設定されています)。
public class Demo {
private static void run() {
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
System.out.println("Done!");
}
}
以下のコマンドラインオプションを使用します。
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run
そして、私のrunメソッドには、C1とC2で適切にコンパイルされたさまざまなバージョンがあります。私にとっては、最後の変種(C2)は次のようになります。
...
; B1: # B3 B2 <- BLOCK HEAD IS JUNK Freq: 1
0x00000000125461b0: mov dword ptr [rsp+0ffffffffffff7000h], eax
0x00000000125461b7: Push rbp
0x00000000125461b8: sub rsp, 40h
0x00000000125461bc: mov ebp, dword ptr [rdx]
0x00000000125461be: mov rcx, rdx
0x00000000125461c1: mov r10, 57fbc220h
0x00000000125461cb: call indirect r10 ; *iload_1
0x00000000125461ce: cmp ebp, 7fffffffh ; 7fffffff => 2147483647
0x00000000125461d4: jnl 125461dbh ; jump if not less
; B2: # B3 <- B1 Freq: 0.999999
0x00000000125461d6: mov ebp, 7fffffffh ; *if_icmpge
; B3: # N44 <- B1 B2 Freq: 1
0x00000000125461db: mov edx, 0ffffff5dh
0x0000000012837d60: nop
0x0000000012837d61: nop
0x0000000012837d62: nop
0x0000000012837d63: call 0ae86fa0h
...
ちょっと面倒ですが、よく見ると、ここには実行中のループがないことに気付くかもしれません。 B1、B2、B3の3つのブロックがあり、実行ステップはB1 -> B2 -> B3
またはB1 -> B3
です。 Freq: 1
- ブロック実行の正規化推定頻度。
あなたはループが何もしないことを検出するのにかかる時間を測定していて、バックグラウンドスレッドでコードをコンパイルして、そしてコードを排除します。
for (int t = 0; t < 5; t++) {
long start = System.nanoTime();
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
long time = System.nanoTime() - start;
String s = String.format("%d: Took %.6f ms", t, time / 1e6);
Thread.sleep(50);
System.out.println(s);
Thread.sleep(50);
}
-XX:+PrintCompilation
を付けてこれを実行すると、コードがレベル3またはC1コンパイラへのバックグラウンドでコンパイルされ、C4のレベル4への数回のループの後にコンパイルされたことがわかります。
129 34 % 3 A::main @ 15 (93 bytes)
130 35 3 A::main (93 bytes)
130 36 % 4 A::main @ 15 (93 bytes)
131 34 % 3 A::main @ -2 (93 bytes) made not entrant
131 36 % 4 A::main @ -2 (93 bytes) made not entrant
0: Took 2.510408 ms
268 75 % 3 A::main @ 15 (93 bytes)
271 76 % 4 A::main @ 15 (93 bytes)
274 75 % 3 A::main @ -2 (93 bytes) made not entrant
1: Took 5.629456 ms
2: Took 0.000000 ms
3: Took 0.000364 ms
4: Took 0.000365 ms
ループをlong
を使用するように変更した場合、最適化されたものにはなりません。
for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
代わりにあなたが得る
0: Took 1579.267321 ms
1: Took 1674.148662 ms
2: Took 1885.692166 ms
3: Took 1709.870567 ms
4: Took 1754.005112 ms