彼の投稿の解説で、パフォーマンスが重要な場合、ラッパーを単一要素の配列に置き換えることができると述べました。配列要素へのアクセスは、インスタンスフィールドから値を抽出するよりも定義により高速です。
別のパフォーマンスホワイトペーパーを読んだ後、このステレオタイプをSunから継承しました。 そこには、ローカル変数が最も速くアクセスされ、その後に静的フィールド、次に配列要素がアクセスされ、このインスタンスフィールドリストが閉じられることが記述されています。
幸いなことに、彼らは私の言葉を信じなかったので、これがこの記事を書く理由でした。
この記事はジュニア向けではなく、Java、ASM x86、およびバイトコードを知っている必要があります。
オブジェクトフィールドへのアクセスと比較して、配列要素へのアクセスが高速かどうかを理解するために、次のことができます。
- 総合的な性能テストを書く
- 実際に配列[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パフォーマンスベンチマークを開始しましたが、違いはありません。