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 < n
とi++ < n
の違いは、結果に大きな影響を与えるのに十分な大きさでした。
バイトコードを見ると、高速バージョンのループ本体は次のとおりです。
iinc
iload
ldc
if_icmplt
遅いバージョンの場合:
iload
iinc
ldc
if_icmplt
そのため、++i < n
の場合、最初にローカル変数i
を1インクリメントし、次にi++ < n
がこれら2つのステップを逆の順序で実行している間にそれをオペランドスタックにプッシュします。しかし、前者のほうがずっと速い理由は説明できないようです。後者の場合に関係する一時的なコピーはありますか?または、パフォーマンスの違いの原因となるのは、バイトコード(VM実装、ハードウェアなど)を超えたものですか?
私は++i
およびi++
に関する他の議論を読みましたが、Java固有であり、++i
またはi++
は値の比較に関与します。
他の人が指摘したように、テストには多くの点で欠陥があります。
このテストを行った正確な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} 'runPreIncrement' '()V' in 'PrePostIncrement'
# [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} 'runPostIncrement' '()V' in 'PrePostIncrement'
# [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は神秘的な方法で動作します...
++ 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を使用した場合と同じパフォーマンスで使用できるはずです。
EDIT 2
あなたは本当にここを見てください:
[〜#〜] 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
[スコア]フィールドは、関心のあるものです。
おそらくこのテストは結論を出すのに十分ではありませんが、この場合、i ++の保存値(事前値)はこのループで使用されないため、JVMはi ++を++ iに変更することでこの式を最適化できます。