在研究分代垃圾回收器对应用程序性能的微妙影响时,我发现在写入基本操作 - 写入堆位置 - 时与写入原语或引用有关,性能存在惊人的差异。
微基准测试
@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倍以上。有什么可能解释这样的减速呢?
原始数组的写入速度超过每纳秒10次写入。也许我应该问一个相反的问题:是什么使原始写入如此快呢?(顺便说一句,我已经检查过,时间与数组大小成线性比例。)
请注意,这都是单线程的;指定@Threads(2)将增加两个测量值,但比率将类似。
背景知识:卡表及其关联的写屏障
Young代中的对象可能只能从Old代中的对象访问。为了避免收集活动对象,YG收集器必须知道自上次YG收集以来写入Old代区域的任何引用。这是通过一种称为“脏标记表”的“脏标记表”实现的,每512字节堆块有一个标记。
这个方案的“丑陋”部分在于我们意识到每个引用的每个写入都必须伴随着一段维护“卡表不变式”的代码:保护正在写入的地址的卡表位置必须被标记为“脏”。这段代码被称为“写入屏障”。
具体的机器码如下所示:
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
写入障碍似乎只增加了一个写操作,但我的测量结果显示它导致了数量级的减速。我无法解释这一点。
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
Arrays.fill
的本地代码。似乎HotSpot能够证明它需要做的一切。 - Marko TopolnikreferenceArray
和primitiveArray
是静态的?这将会影响到使用@Threads(2)
时的结果。 - Aleksey Shipilev@Setup(Iteration)
生成大量垃圾以增加GC压力。 - Aleksey Shipilev