配列要素へのアクセスがオブジェクトのフィールドへのアクセスよりも速い理由の質問への回答

最近では、親愛なるlanyがJavaの可変数に関するすばらしい投稿を書いています: http : //habrahabr.ru/post/151887/

彼の投稿の解説で、パフォーマンスが重要な場合、ラッパーを単一要素の配列に置き換えることができると述べました。配列要素へのアクセスは、インスタンスフィールドから値を抽出するよりも定義により高速です。

別のパフォーマンスホワイトペーパーを読んだ後、このステレオタイプをSunから継承しました。 そこには、ローカル変数が最も速くアクセスされ、その後に静的フィールド、次に配列要素がアクセスされ、このインスタンスフィールドリストが閉じられることが記述されています。

幸いなことに、彼らは私の言葉を信じなかったので、これがこの記事を書く理由でした。

この記事はジュニア向けではなく、Java、ASM x86、およびバイトコードを知っている必要があります。





オブジェクトフィールドへのアクセスと比較して、配列要素へのアクセスが高速かどうかを理解するために、次のことができます。

  1. 総合的な性能テストを書く
  2. 実際に配列[0]とthis.valueの背後にあるものを理解する


すでに病気になっている合成テストから、2番目の方法を選択しました。 最初に、テストクラスをスローします。

package numbers; public class TestNumbers { public static void main(String[] args) { test(new IntNumField()); test(IntNumArray.create()); } private static final void test(int[] mutableInt2) { for (int index = 10000; index-- > 0;) { IntNumArray.inc(mutableInt2); } } private static final void test(IntNumField mutableInt1) { for (int index = 10000; index-- > 0;) { mutableInt1.inc(); } } public static final class IntNumField { protected int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } public final void inc() { this.value++; } } public static final class IntNumArray { public static final int[] create() { return new int[1]; } public static final int get(int[] array) { return array[0]; } public static final void set(int[] array, int value) { array[0] = value; } public static final void inc(int[] array) { array[0]++; } } }
      
      







IntNumFieldクラスは、オブジェクトのフィールドで動作し、IntNumArrayは、私が提案したように、シングルトン配列で動作します。 各クラスには、変更可能な変数を1つずつ増やすincメソッドが含まれています。 テストメソッドでは、incの各バージョンが10,000回呼び出されます。これにより、メソッドのコンパイルが保証されます。



しかし、最初に、バイトコードを見てください。

 ~$ javap -c numbers/TestNumbers$IntNumField public int getValue(); Code: 0: aload_0 1: getfield #18; //Field value:I 4: ireturn public void setValue(int); Code: 0: aload_0 1: iload_1 2: putfield #18; //Field value:I 5: return public final void inc(); Code: 0: aload_0 1: dup 2: getfield #18; //Field value:I 5: iconst_1 6: iadd 7: putfield #18; //Field value:I 10: return
      
      







getメソッドでは、すべてが単純で、これをロードして、値valueをスタックにプッシュします。 残りはほぼ同じですが、incはdupを使用して最適化します。

IntNumArrayクラスで何が待っているか見てみましょう。

 ~$ javap -c numbers/TestNumbers$IntNumArray public static final int get(int[]); Code: 0: aload_0 1: iconst_0 2: iaload 3: ireturn public static final void set(int[], int); Code: 0: aload_0 1: iconst_0 2: iload_1 3: iastore 4: return public static final void inc(int[]); Code: 0: aload_0 1: iconst_0 2: dup2 3: iaload 4: iconst_1 5: iadd 6: iastore 7: return
      
      







getfieldがiconst_0およびialoadに置き換えられていることを除いて、ほぼ同じことです。 まだわかりにくいので、特別なパラメーターを使用してincメソッドのHotSpotを生成するアセンブラーを見てください。

 ~$ java -XX:+UnlockDiagnosticVMOptions -XX:CompileThreshold=10000 -XX:+PrintAssembly numbers.TestNumbers # {method} 'inc' '()V' in 'numbers/TestNumbers$IntNumField' # [sp+0x20] (sp of caller) 0x02141a87: cmp 0x4(%ecx),%eax 0x02141a8a: jne 0x020dd100 ; {runtime_call} [Verified Entry Point] 0x02141a90: mov %eax,0xffffc000(%esp) 0x02141a97: push %ebp 0x02141a98: sub $0x18,%esp ;*aload_0 ; - numbers.TestNumbers$IntNumField::inc@0 (line 50) 0x02141a9b: mov 0x8(%ecx),%esi ;*getfield value ; - numbers.TestNumbers$IntNumField::inc@2 (line 50) 0x02141a9e: inc %esi 0x02141a9f: mov %esi,0x8(%ecx) ;*putfield value ; - numbers.TestNumbers$IntNumField::inc@7 (line 50) 0x02141aa2: add $0x18,%esp 0x02141aa5: pop %ebp 0x02141aa6: test %eax,0x1b0100 ; {poll_return} 0x02141aac: ret # {method} 'inc' '([I)V' in 'numbers/TestNumbers$IntNumArray' # parm0: ecx = '[I' # [sp+0x20] (sp of caller) 0x02141c80: mov %eax,0xffffc000(%esp) 0x02141c87: push %ebp 0x02141c88: sub $0x18,%esp ;*aload_0 ; - numbers.TestNumbers$IntNumArray::inc@0 (line 71) 0x02141c8b: cmpl $0x0,0x8(%ecx) ; implicit exception: dispatches to 0x02141cb7 0x02141c92: jbe 0x02141cc1 0x02141c98: mov 0xc(%ecx),%esi ;*iaload ; - numbers.TestNumbers$IntNumArray::inc@3 (line 71) 0x02141c9b: inc %esi 0x02141c9c: cmpl $0x0,0x8(%ecx) 0x02141ca3: jbe 0x02141ccd 0x02141ca9: mov %esi,0xc(%ecx) ;*iastore ; - numbers.TestNumbers$IntNumArray::inc@6 (line 71) 0x02141cac: add $0x18,%esp 0x02141caf: pop %ebp 0x02141cb0: test %eax,0x1b0100 ; {poll_return} 0x02141cb6: ret
      
      







[Exception Handler]セクションでコードの配置やコードの束などをスローして、両方のメソッドをカットする必要がありました。

よく見ると、両方のメソッドはinc%esiを使用してスタックの値を増やします。 異なるだけ

 mov 0x8(%ecx),%esi ;*getfield value
      
      





そして

 cmpl $0x0,0x8(%ecx) ; implicit exception: dispatches to 0x02141cb7 jbe 0x02141cc1 mov 0xc(%ecx),%esi ;*iaload
      
      





コードの2番目の部分の最初の2行は、インデックス0が配列の境界を超えないことのチェックです(0 <array.length)。 彼女のせいで、私が間違っていたことがわかりました。さらに悪いことに、以下の2行でまったく同じチェックが行われます。これが遅くなることはほとんどありませんが、オブジェクトのフィールドにアクセスするよりも速くなりません。



疑ってくれたアパンギンに感謝します。これは私たち両方にとって興味深いだけではありません。 ラッパークラスとOOPのすべての魅力を安全に使用できます。



PSパフォーマンスベンチマークを開始しましたが、違いはありません。



All Articles