Mar*_*nik 9 java garbage-collection microbenchmark jmh
在研究分代垃圾收集器对应用程序性能的微妙后果时,我已经在一个非常基本的操作 - 一个简单的写入堆位置 - 的性能方面遇到了相当惊人的差异 - 关于所写的值是原始值还是引用.
@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;
}
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
由于整个循环几乎慢了8倍,因此写入本身可能慢了10倍以上.什么可以解释这种放缓?
写出原始数组的速度是每纳秒超过10次写入.也许我应该问我问题的另一面:是什么让原始写作如此之快?(顺便说一句,我已经检查过,时间与数组大小呈线性关系.)
请注意,这都是单线程的; 指定@Threads(2)将增加两个测量值,但比率将相似.
Young Generation中的一个对象可能恰好只能从Old Generation中的一个对象到达.为避免收集活动对象,YG收集器必须知道自上次YG收集以来写入旧生成区域的任何引用.这是通过一种称为" 卡表"的"脏标志表"实现的,该表具有每个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
Run Code Online (Sandbox Code Playgroud)
当写入的值是原始的时,这就是同一个高级操作所需要的:
mov [edx+ebx*4+0x10], ebp
Run Code Online (Sandbox Code Playgroud)
写入障碍似乎只"贡献"了一次写入,但我的测量表明它会导致数量级的减速.我无法解释这一点.
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
Run Code Online (Sandbox Code Playgroud)
hotspot-compiler-dev:嗨,马科,
对于原始数组,我们使用手写汇编代码,该代码使用 XMM 寄存器作为初始化向量。对于对象数组,我们没有对其进行优化,因为它并不常见。我们可以像对 arracopy 所做的那样改进它,但我们决定暂时保留它。
问候,
弗拉基米尔
我还想知道为什么优化的代码没有内联,并且也得到了答案:
代码并不小,所以我们决定不内联它。查看macroAssembler_x86.cpp中的MacroAssembler::generate_fill():
我错过了机器代码中的一个重要位,显然是因为我正在查看已编译方法的堆栈替换版本,而不是用于后续调用的版本。事实证明,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
Run Code Online (Sandbox Code Playgroud)
循环和调用的结果fill是相同的。如果说有什么不同的话,那就是这比引发这个问题的结果更令人困惑。无论值类型如何,我至少期望fill从相同的优化思想中受益。
| 归档时间: |
|
| 查看次数: |
303 次 |
| 最近记录: |