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


113

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);
}

69
はい、そうです。ループ本体には副作用がないため、コンパイラーはそれを非常に喜んで排除します。バイトコードjavap -vを調べて確認してください。
エリオットフリッシュ2017

36
あなたはバイトコードに戻ってそれを見ることはありません。javac実際の最適化はほとんど行わず、そのほとんどをJITコンパイラーに任せます。
Jorn Vernee 2017

4
「これがその下のJVM最適化の結果かどうか疑問に思いますか?」- どう思いますか?JVM最適化でなければ、他に何ができるでしょうか?
apangin 2017

7
この質問への回答は、基本的にstackoverflow.com/a/25323548/3182664に含まれています。また、そのような場合にJITが生成する結果のアセンブリ(マシンコード)も含まれ、ループがJITによって完全に最適化されることを示しています。(stackoverflow.com/q/25326377/3182664での質問は、ループが40億回の演算を行わない場合、40億マイナス1を実行すると少し時間がかかる可能性があることを示しています;-))。私はこの質問を他の質問の複製とほぼみなします-異論はありますか?
Marco13

7
プロセッサーがHzごとに1回の反復を実行すると想定します。それは遠大な前提です。今日のプロセッサーは、@ Rahulが述べたように、あらゆる種類の最適化を実行します。Corei7がどのように機能するかについて詳しく知らない限り、それを想定することはできません。
Tsahi Asher

回答:


106

ここで行われている可能性は2つあります。

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

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

最新のコンパイラーは非常にインテリジェントです。彼らはコードが役に立たないときを見ることができます。GodBoltに空のループを入れて出力を確認し、-O2最適化をオンにすると、出力が次のように表示されます。

main():
    xor eax, eax
    ret

はっきりさせておきたいのですが、Javaではほとんどの最適化はJITによって行われます。他の一部の言語(C / C ++など)では、ほとんどの最適化は最初のコンパイラーによって行われます。


コンパイラはそのような最適化を行うことを許可されていますか?Javaについてはよくわかりませんが、.NETコンパイラは、JITがプラットフォームに最適な最適化を行えるようにするために、通常これを回避する必要があります。
IllidanS4はモニカに2017

1
@ IllidanS4一般に、これは言語標準に依存します。コンパイラが最適化を実行できるということは、標準によって解釈されたコードが同じ効果を持つことを意味し、そうです。ただし、考慮しなければならない微妙な点はたくさんあります。たとえば、浮動小数点計算の変換によってオーバーフロー/アンダーフローが発生する可能性があるため、最適化は慎重に行う必要があります。
user1997744 2017

9
@ IllidanS4ランタイム環境でより適切な最適化を行うにはどうすればよいですか?少なくとも、コンパイル中にコードを削除するよりも速くできないコードを分析する必要があります。
Gerhardh 2017

2
@Gerhardhランタイムがコードの冗長な部分を削除することでより良い仕事をすることができないとき、私はこの正確なケースについて話していませんでしたが、もちろんこの理由が正しい場合もあります。また、他の言語からのJRE用の他のコンパイラが存在する可能性があるため、ランタイムもこれらの最適化を行う必要があるため、ランタイムとコンパイラの両方がそれらを実行する理由はない可能性があります。
IllidanS4はモニカに2017

6
@ IllidanS4ランタイムの最適化にかかる時間はゼロ未満にはなりません。コンパイラがコードを削除しないようにしても意味がありません。
Gerhardh 2017

55

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

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

OPのコードをの中に入れましたclass MyClass


2
変だ。両方の方法でコードを実行すると、フラグを使用しない方高速ですが、係数は10であり、ループの反復回数にゼロを追加または削除すると、実行時間に影響を及ぼします。国旗。だから(私にとって)ループは完全に最適化されているようには見えず、どういうわけか10倍速くなっただけです。(Oracle Java 8-151)
tobias_k 2017

@tobias_kループが通過するJITのステージによって異なります stackoverflow.com/a/47972226/1059372を
ユージン

21

私は明白なことだけ述べます-これは発生するJVM最適化であり、ループは単に削除されるだけです。以下の小さなテストは、有効化/有効化のみ、無効化した場合の大きな違いを示しています。JITC1 Compiler

免責事項:このようなテストを記述しないでください。これは、実際のループの「削除」がで発生することを証明するためだけ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 

13

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

実際には、2つのJITコンパイラーC1およびC2があります。まず、コードはC1でコンパイルされます。C1は統計を収集し、JVMが100%の場合、空のループは何も変更せず、役に立たないことを発見するのに役立ちます。この状況では、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

そして、C1とC2で適切にコンパイルされた私のrunメソッドにはさまざまなバージョンがあります。私にとって、最終的なバリアント(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です。Where- Freq: 1ブロック実行の正規化された推定頻度。


8

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

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コンパイラにコンパイルされ、数回のループの後にレベル4のC4にコンパイルされていることがわかります。

    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

それは奇妙です...なぜlongカウンターは同じ最適化が起こらないようにするのでしょうか?
Ryan Amos

@RyanAmos最適化は、タイプintノートcharとshortがバイトコードレベルで実質的に同じである場合にのみ、共通のプリミティブループカウントに適用されます。
Peter Lawrey

-1

開始時間と終了時間をナノ秒で考慮し、10 ^ 6で除算してレイテンシを計算します

long d = (finish - start) / 1000000

それはする必要があります10^9ので、1第二= 10^9ナノ秒。


あなたの提案は私の考えとは無関係です。私が不思議に思っていたのは、どれくらいの時間がかかったのかであり、この期間がミリ秒または秒のどちらで表示/表現されているかは関係ありません。
twimo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.