"while (i++ < n) {}" が "while (++i < n) {}" より著しく遅いのはなぜ?
疑問点
私の Windows 8 ラップトップで、HotSpot JDK 1.7.0_45 (すべてのコンパイラー/VM オプションをデフォルトに設定) を使用して、以下のループが発生するようです。
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}
は、少なくとも2桁以上速い(~10ms vs. ~5000ms)ことがわかります。
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++
は値の比較に関与しています。
どのように解決するのですか?
他の人が指摘しているように、このテストは多くの点で欠陥があります。
あなたは正確に教えてくれませんでしたが どのように を正確に教えてくれませんでした。しかし、私はこのような(悪気はないのですが)素朴なテストを実装しようとしました。
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
フラグで実行したときに明らかになります。私の場合、結果は次のようなものに沿っています。
...
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
なぜ、このようなことをするのか、私にはまったくわかりません。 ではなく はインクリメント後のバージョンを削除します。(実際、私はこれを別の質問として尋ねることを検討しています)。しかし、少なくとも、これはなぜ桁違いの違いが見られるのかを説明するものです。
EDIT: 興味深いことに、ループの上限を
Integer.MAX_VALUE
から
Integer.MAX_VALUE-1
に変更すると
ともに
の両方のバージョンは最適化され、必要な時間はゼロになります。どういうわけか、この制限 (これはまだ
0x7fffffff
として表示されます) が最適化を妨げています。おそらく、これは比較が(歌われた!)cmp
命令にマッピングされることと関係があると思われますが、それ以上の深い理由は説明できません。JIT は不思議な方法で動作します...
関連
-
StringBuilderが投げるArrayIndexOutOfBoundsExceptionの探索
-
Zipファイルの圧縮・解凍にantを使用する
-
[解決済み] この2回(1927年)を引き算すると、なぜおかしな結果になるのでしょうか?
-
[解決済み] なぜパスワードにはStringではなくchar[]が好まれるのですか?
-
[解決済み] SQLiteのINSERT/per-secondのパフォーマンスを向上させる
-
[解決済み] B "の印刷が "#"の印刷より劇的に遅いのはなぜですか?
-
[解決済み] 要素ごとの加算は、結合ループよりも分離ループの方がはるかに高速なのはなぜですか?
-
[解決済み] なぜGCCはa*a*a*a*aを(a*a*a)*(a*a*a)に最適化しないのでしょうか?
-
[解決済み] Intel CPU の _mm_popcnt_u64 で、32 ビットのループカウンターを 64 ビットに置き換えると、パフォーマンスが著しく低下します。
-
[解決済み】なぜJavaの+=, -=, *=, /=複合代入演算子はキャスティングを必要としないのですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
undefined[sonar] sonar:デフォルトのスキャンルール
-
java.sql.SQLException: executeQuery()でデータ操作文を発行できません。
-
Collections.sortがdoubleでソートできない問題を完璧に解決する。
-
javaの非静的メソッドを静的に参照することができない
-
Javaクラスが "Error occurred during initialization of boot layer "というエラーで実行される。
-
JDK8 の Optional.of と Optional.ofNullable メソッドの違いと使い方を説明する。
-
eclipse にリソースリーク:'in' が閉じない
-
コンストラクタDate()が未定義である問題
-
Web Project JavaでPropertiesファイルを読み込むと、「指定されたファイルがシステムで見つかりません」というソリューションが表示されます。
-
linux ant Resolve error: main class not found or couldn't be loaded org.apache.tools.ant.launcher.