web-dev-qa-db-ja.com

40億反復のJavaループに2msしかかからないのはなぜですか?

私は、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);
}
112
twimo

ここでは2つの可能性があります。

  1. コンパイラは、ループが冗長で何もしていないことに気付いたため、ループを最適化しました。

  2. JIT(ジャストインタイムコンパイラ)は、ループが冗長で何もしないことに気付いたため、最適化しました。

現代のコンパイラは非常に知的です。彼らはコードが役に立たなくなったときにそれを見ることができます。空のループを GodBolt に入れて出力を確認し、次に-O2の最適化をオンにすると、出力は次のようになります。

main():
    xor eax, eax
    ret

私は何かを明確にしたいのですが、Javaではほとんどの最適化はJITによって行われます。他の言語(C/C++など)では、ほとんどの最適化は最初のコンパイラーによって行われます。

105
van dench

JITコンパイラによって最適化されたようです。オフにすると(-Djava.compiler=NONE)、コードの実行速度が大幅に低下します。

$ javac MyClass.Java
$ Java MyClass
Used 4
$ Java -Djava.compiler=NONE MyClass
Used 40409

私はOPのコードをclass MyClassの中に入れます。

54
Akavall

明白なことを述べるつもりです - これが起こる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 
20
Eugene

すでに指摘したように、JIT(ジャストインタイム)コンパイラは、不要な繰り返しを削除するために空のループを最適化できます。しかし、どうですか?

実際には、JITコンパイラは2つあります。C1C2。まず、コードを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 - ブロック実行の正規化推定頻度。

12
Oleksandr

あなたはループが何もしないことを検出するのにかかる時間を測定していて、バックグラウンドスレッドでコードをコンパイルして、そしてコードを排除します。

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
8
Peter Lawrey