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