9

世代別ガベージ コレクターがアプリケーションのパフォーマンスに及ぼす微妙な影響を調査しているときに、書き込まれた値がプリミティブか参照かに関して、非常に基本的な操作 (ヒープの場所への単純な書き込み) のパフォーマンスに驚くべき矛盾があることに気付きました。

マイクロベンチマーク

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1)
@State(Scope.Thread)
@Threads(1)
@Fork(2)
public class Writing
{
  static final int TARGET_SIZE = 1024;

  static final    int[] primitiveArray = new    int[TARGET_SIZE];
  static final Object[] referenceArray = new Object[TARGET_SIZE];

  int val = 1;
  @GenerateMicroBenchmark
  public void fillPrimitiveArray() {
    final int primitiveValue = val++;
    for (int i = 0; i < TARGET_SIZE; i++)
      primitiveArray[i] = primitiveValue;
  }

  @GenerateMicroBenchmark
  public void fillReferenceArray() {
    final Object referenceValue = new Object();
    for (int i = 0; i < TARGET_SIZE; i++)
      referenceArray[i] = referenceValue;
  }
}

結果

Benchmark              Mode Thr    Cnt  Sec         Mean   Mean error    Units
fillPrimitiveArray     avgt   1      6    1       87.891        1.610  nsec/op
fillReferenceArray     avgt   1      6    1      640.287        8.368  nsec/op

ループ全体がほぼ 8 倍遅いため、書き込み自体はおそらく 10 倍以上遅くなります。何がこのような減速を説明できるのでしょうか?

プリミティブ配列の書き込み速度は、1 ナノ秒あたり 10 回を超えます。おそらく、私は自分の質問の反対側を尋ねるべきです:何が原始的な書き込みをそんなに速くするのですか? (ところで、私がチェックしたところ、時間は配列サイズに比例してスケーリングします。)

これはすべてシングルスレッドであることに注意してください。指定する@Threads(2)と両方の測定値が増加しますが、比率は似ています。


ちょっとした背景:カード テーブルと関連する書き込みバリア

Young 世代のオブジェクトは、Old 世代のオブジェクトからのみ到達可能である可能性があります。ライブ オブジェクトの収集を避けるために、YG コレクターは、最後の YG コレクション以降に旧世代領域に書き込まれたすべての参照を認識している必要があります。これは、カード テーブルと呼ばれる一種の「ダーティ フラグ テーブル」で実現されます。このテーブルには、512 バイトのヒープのブロックごとに 1 つのフラグがあります。

スキームの「醜い」部分は、参照のすべての書き込みにカードテーブル不変条件が付随しなければならないことに気付いたときに発生します-コードの一部を維持します: 書き込まれるアドレスを保護するカードテーブル内の場所をマークする必要があります汚れています。このコードは書き込みバリアと呼ばれます。

特定のマシンコードでは、これは次のようになります。

lea   edx, [edi+ebp*4+0x10]   ; calculate the heap location to write
mov   [edx], ebx              ; write the value to the heap location
shr   edx, 9                  ; calculate the offset into the card table
mov   [ecx+edx], ah           ; mark the card table entry as dirty

書き込まれた値がプリミティブである場合、同じ高レベルの操作に必要なのはこれだけです。

mov   [edx+ebx*4+0x10], ebp

書き込みバリアは、「ちょうど」もう 1 つの書き込みに寄与しているように見えますが、私の測定では、1 桁の速度低下を引き起こすことが示されています。これは説明できません。

UseCondCardMark悪化させるだけ

エントリがすでにダーティとマークされている場合、カード テーブルへの書き込みを回避するはずの、非常にわかりにくい JVM フラグがあります。これは主に、多くのカード テーブルへの書き込みによってCPU キャッシュを介したスレッド間での誤った共有が発生する、いくつかの縮退したケースで重要です。とにかく、私はそのフラグをオンにしてみました:

with  -XX:+UseCondCardMark:
Benchmark              Mode Thr    Cnt  Sec         Mean   Mean error    Units
fillPrimitiveArray     avgt   1      6    1       89.913        3.586  nsec/op
fillReferenceArray     avgt   1      6    1     1504.123       12.130  nsec/op
4

1 に答える 1

4

hotspot-compiler-devメーリング リストで Vladimir Kozlov によって提供された信頼できる回答を引用します。

こんにちはマルコ、

プリミティブ配列の場合、XMM レジスタを初期化のベクトルとして使用する手書きのアセンブラー コードを使用します。オブジェクト配列については、一般的なケースではないため、最適化しませんでした。arracopy で行ったのと同じように改善できますが、今はそのままにしておくことにしました。

よろしく、
ウラジミール

また、最適化されたコードがインライン化されていない理由も疑問に思っており、その答えも得ました。

コードは小さくないため、インライン化しないことにしました。macroAssembler_x86.cpp の MacroAssembler::generate_fill() を見てください。

http://hg.openjdk.java.net/hsx/hotspot-main/hotspot/file/54f0c207dc35/src/cpu/x86/vm/macroAssembler_x86.cpp


私の元の答え:

後続の呼び出しに使用されるメソッドではなく、コンパイルされたメソッドのオンスタック置換バージョンを見ていたため、マシン コードの重要な部分を見逃していたようです。HotSpot は、私のループが への呼び出しで実行されることになることを証明し、ループ全体をそのようなコードへの命令にArrays.fill置き換えたことがわかります。callその関数のコードはわかりませんが、MMX 命令など、考えられるすべてのトリックを使用して、同じ 32 ビット値でメモリ ブロックを埋めているのでしょう。

これにより、実際のArrays.fill通話を測定するというアイデアが得られました。私はさらに驚きました:

Benchmark                  Mode Thr    Cnt  Sec         Mean   Mean error    Units
fillPrimitiveArray         avgt   1      5    2      155.343        1.318  nsec/op
fillReferenceArray         avgt   1      5    2      682.975       17.990  nsec/op
loopFillPrimitiveArray     avgt   1      5    2      156.114        0.523  nsec/op
loopFillReferenceArray     avgt   1      5    2      682.209        7.047  nsec/op

ループと呼び出しの結果fillは同じです。どちらかといえば、これは質問の動機となった結果よりもさらに混乱を招きます。値のタイプに関係なく、少なくともfill同じ最適化のアイデアから利益が得られると期待していました。

于 2014-02-03T10:38:02.770 に答える