web-dev-qa-db-ja.com

「while(i ++ <n){}」が「while(++ i <n){}」よりも大幅に遅い理由

HotSpot JDK 1.7.0_45(すべてのコンパイラ/ VMオプションがデフォルトに設定されている)を搭載したWindows 8ラップトップでは、以下のループ

final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}

以下より少なくとも2桁高速です(〜10 ms対〜5000 ms)。

final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {
}

別の無関係なパフォーマンスの問題を評価するためのループを書いているときに、この問題に気づきました。また、++i < ni++ < nの違いは、結果に大きな影響を与えるのに十分な大きさでした。

バイトコードを見ると、高速バージョンのループ本体は次のとおりです。

iinc
iload
ldc
if_icmplt

遅いバージョンの場合:

iload
iinc
ldc
if_icmplt

そのため、++i < nの場合、最初にローカル変数iを1インクリメントし、次にi++ < nがこれら2つのステップを逆の順序で実行している間にそれをオペランドスタックにプッシュします。しかし、前者のほうがずっと速い理由は説明できないようです。後者の場合に関係する一時的なコピーはありますか?または、パフォーマンスの違いの原因となるのは、バイトコード(VM実装、ハードウェアなど)を超えたものですか?

私は++iおよびi++に関する他の議論を読みましたが、Java固有であり、++iまたはi++は値の比較に関与します。

74
sikan

他の人が指摘したように、テストには多くの点で欠陥があります。

このテストを行った正確なhowを教えていない。ただし、次のような「単純な」テスト(違反なし)を実装しようとしました。

class PrePostIncrement
{
    public static void main(String args[])
    {
        for (int j=0; j<3; j++)
        {
            for (int i=0; i<5; i++)
            {
                long before = System.nanoTime();
                runPreIncrement();
                long after = System.nanoTime();
                System.out.println("pre  : "+(after-before)/1e6);
            }
            for (int i=0; i<5; i++)
            {
                long before = System.nanoTime();
                runPostIncrement();
                long after = System.nanoTime();
                System.out.println("post : "+(after-before)/1e6);
            }
        }
    }

    private static void runPreIncrement()
    {
        final int n = Integer.MAX_VALUE;
        int i = 0;
        while (++i < n) {}
    }

    private static void runPostIncrement()
    {
        final int n = Integer.MAX_VALUE;
        int i = 0;
        while (i++ < n) {}
    }
}

これをデフォルト設定で実行すると、わずかな違いがあるようです。ただし、-serverフラグを指定して実行すると、ベンチマークのrealの欠陥が明らかになります。私の場合の結果は、次のようなものに沿っています

...
pre  : 6.96E-4
pre  : 6.96E-4
pre  : 0.001044
pre  : 3.48E-4
pre  : 3.48E-4
post : 1279.734543
post : 1295.989086
post : 1284.654267
post : 1282.349093
post : 1275.204583

明らかに、プリインクリメントバージョンは完全に最適化されています。理由はかなり単純です。結果は使用されません。ループが実行されるかどうかはまったく関係ないため、JITはループを単に削除します。

これは、ホットスポットの逆アセンブリを見ると確認できます。プリインクリメントバージョンでは、次のコードになります。

[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x0000000055060500} &apos;runPreIncrement&apos; &apos;()V&apos; in &apos;PrePostIncrement&apos;
  #           [sp+0x20]  (sp of caller)
  0x000000000286fd80: sub    $0x18,%rsp
  0x000000000286fd87: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - PrePostIncrement::runPreIncrement@-1 (line 28)

  0x000000000286fd8c: add    $0x10,%rsp
  0x000000000286fd90: pop    %rbp
  0x000000000286fd91: test   %eax,-0x243fd97(%rip)        # 0x0000000000430000
                                                ;   {poll_return}
  0x000000000286fd97: retq   
  0x000000000286fd98: hlt    
  0x000000000286fd99: hlt    
  0x000000000286fd9a: hlt    
  0x000000000286fd9b: hlt    
  0x000000000286fd9c: hlt    
  0x000000000286fd9d: hlt    
  0x000000000286fd9e: hlt    
  0x000000000286fd9f: hlt    

ポストインクリメントバージョンの結果は次のコードになります。

[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x00000000550605b8} &apos;runPostIncrement&apos; &apos;()V&apos; in &apos;PrePostIncrement&apos;
  #           [sp+0x20]  (sp of caller)
  0x000000000286d0c0: sub    $0x18,%rsp
  0x000000000286d0c7: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - PrePostIncrement::runPostIncrement@-1 (line 35)

  0x000000000286d0cc: mov    $0x1,%r11d
  0x000000000286d0d2: jmp    0x000000000286d0e3
  0x000000000286d0d4: nopl   0x0(%rax,%rax,1)
  0x000000000286d0dc: data32 data32 xchg %ax,%ax
  0x000000000286d0e0: inc    %r11d              ; OopMap{off=35}
                                                ;*goto
                                                ; - PrePostIncrement::runPostIncrement@11 (line 36)

  0x000000000286d0e3: test   %eax,-0x243d0e9(%rip)        # 0x0000000000430000
                                                ;*goto
                                                ; - PrePostIncrement::runPostIncrement@11 (line 36)
                                                ;   {poll}
  0x000000000286d0e9: cmp    $0x7fffffff,%r11d
  0x000000000286d0f0: jl     0x000000000286d0e0  ;*if_icmpge
                                                ; - PrePostIncrement::runPostIncrement@8 (line 36)

  0x000000000286d0f2: add    $0x10,%rsp
  0x000000000286d0f6: pop    %rbp
  0x000000000286d0f7: test   %eax,-0x243d0fd(%rip)        # 0x0000000000430000
                                                ;   {poll_return}
  0x000000000286d0fd: retq   
  0x000000000286d0fe: hlt    
  0x000000000286d0ff: hlt    

なぜそれがポストインクリメントバージョンをnot削除しないのか、私には完全には明らかではありません。 (実際、これは別の質問として質問することを検討しています)。しかし、少なくとも、これは「大きさのオーダー」との違いを見る理由を説明しています...


編集:興味深いことに、ループの上限をInteger.MAX_VALUEからInteger.MAX_VALUE-1に変更すると、bothバージョンが最適化され、「ゼロ」時間が必要になります。どういうわけか、この制限(アセンブリでは0x7fffffffとして表示されます)が最適化を妨げています。おそらく、これは(singed!)cmp命令にマップされる比較と関係がありますが、それ以上の深い理由を示すことはできません。 JITは神秘的な方法で動作します...

118
Marco13

++ iとi ++の違いは、++ iが変数を効果的にインクリメントし、その新しい値を「返す」ことです。一方、i ++は、iの現在の値を保持する一時変数を効果的に作成し、変数をインクリメントして一時変数の値を「返します」。これが余分なオーバーヘッドの発生源です。

// i++ evaluates to something like this
// Imagine though that somehow i was passed by reference
int temp = i;
i = i + 1;
return temp;

// ++i evaluates to
i = i + 1;
return i;

あなたの場合、式で結果を使用しているため、増分はJVMによって最適化されないようです。一方、JVMはこのようなループを最適化できます。

for( int i = 0; i < Integer.MAX_VALUE; i++ ) {}

これは、i ++の結果が使用されないためです。このようなループでは、++ iとi ++の両方を、++ iを使用した場合と同じパフォーマンスで使用できるはずです。

19
Smith_61

EDIT 2

あなたは本当にここを見てください:

http://hg.openjdk.Java.net/code-tools/jmh/file/f90aef7f1d2c/jmh-samples/src/main/Java/org/openjdk/jmh/samples/JMHSample_11_Loops.Java

[〜#〜] edit [〜#〜]考えてみると、このテストが何らかの形で間違っていることに気づき、ループが深刻になりますJVMによって最適化されます。

@Paramをドロップして、n=2を許可する必要があると思います。

この方法で、while自体のパフォーマンスをテストします。この場合に得られる結果:

o.m.t.WhileTest.testFirst      avgt         5        0.787        0.086    ns/op
o.m.t.WhileTest.testSecond     avgt         5        0.782        0.087    ns/op

ほとんど違いはありません

最初に自問すべき質問は、これをテストして測定する方法です。これはマイクロベンチマークであり、Javaこれは芸術であり、ほとんどの場合、単純なユーザー(私のような)は結果を間違えます。ベンチマークテストと非常に優れたツールに頼るべきです。 JMHを使用してこれをテストしました。

    @Measurement(iterations=5, time=1, timeUnit=TimeUnit.MILLISECONDS)
@Fork(1)
@Warmup(iterations=5, time=1, timeUnit=TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Benchmark)
public class WhileTest {
    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(".*" + WhileTest.class.getSimpleName() + ".*")
            .threads(1)
            .build();

        new Runner(opt).run();
    }


    @Param({"100", "10000", "100000", "1000000"})
    private int n;

    /*
    @State(Scope.Benchmark)
    public static class HOLDER_I {
        int x;
    }
    */


    @Benchmark
    public int testFirst(){
        int i = 0;
        while (++i < n) {
        }
        return i;
    }

    @Benchmark
    public int testSecond(){
        int i = 0;
        while (i++ < n) {
        }
        return i;
    }
}

JMHでより経験豊富な誰かがこの結果を修正するかもしれません(私はまだそう願っています、私はまだJMHでその汎用性がないので)が、結果はその差がかなり小さいことを示しています:

Benchmark                        (n)   Mode   Samples        Score  Score error    Units
o.m.t.WhileTest.testFirst        100   avgt         5        1.271        0.096    ns/op
o.m.t.WhileTest.testFirst      10000   avgt         5        1.319        0.125    ns/op
o.m.t.WhileTest.testFirst     100000   avgt         5        1.327        0.241    ns/op
o.m.t.WhileTest.testFirst    1000000   avgt         5        1.311        0.136    ns/op
o.m.t.WhileTest.testSecond       100   avgt         5        1.450        0.525    ns/op
o.m.t.WhileTest.testSecond     10000   avgt         5        1.563        0.479    ns/op
o.m.t.WhileTest.testSecond    100000   avgt         5        1.418        0.428    ns/op
o.m.t.WhileTest.testSecond   1000000   avgt         5        1.344        0.120    ns/op

[スコア]フィールドは、関心のあるものです。

18
Eugene

おそらくこのテストは結論を出すのに十分ではありませんが、この場合、i ++の保存値(事前値)はこのループで使用されないため、JVMはi ++を++ iに変更することでこの式を最適化できます。

0
danibuiza