1. ホーム
  2. java

"while (i++ < n) {}" が "while (++i < n) {}" より著しく遅いのはなぜ?

2023-09-22 22:14:10

疑問点

私の 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 < ni++ < n は、結果に大きな影響を与えるほど巨大でした。

バイトコードを見てみると、高速化版のループ本体は

iinc
iload
ldc
if_icmplt

そして低速版には

iload
iinc
ldc
if_icmplt

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

に関する他のいくつかの議論を読みました。 ++ii++ のようなケースに直接関係するような、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} &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    

なぜ、このようなことをするのか、私にはまったくわかりません。 ではなく はインクリメント後のバージョンを削除します。(実際、私はこれを別の質問として尋ねることを検討しています)。しかし、少なくとも、これはなぜ桁違いの違いが見られるのかを説明するものです。


EDIT: 興味深いことに、ループの上限を Integer.MAX_VALUE から Integer.MAX_VALUE-1 に変更すると ともに の両方のバージョンは最適化され、必要な時間はゼロになります。どういうわけか、この制限 (これはまだ 0x7fffffff として表示されます) が最適化を妨げています。おそらく、これは比較が(歌われた!)にマップされることと関係があると思われます。 cmp 命令にマッピングされることと関係があると思われますが、それ以上の深い理由は説明できません。JIT は不思議な方法で動作します...