java - 什么可以解释写入对堆位置的引用的巨大性能损失?

标签 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;
  }
}

结果

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) 将增加两个测量值,但比率将相似。


一些背景知识:卡片表和相关的写屏障

新生代中的对象可能碰巧只能从老年代中的对象访问。为了避免收集 Activity 对象,YG 收集器必须知道自上次 YG 收集以来写入老年代区域的任何引用。这是通过一种称为卡片表的“脏标志表”实现的,它为每个 512 字节堆 block 都有一个标志。

方案的“丑陋”部分是当我们意识到每次写入引用都必须伴随卡片表不变 - 维护代码段时:卡片表中的位置保护被写入地址的地址必须被标记为。这段代码称为写屏障

在特定的机器代码中,这看起来如下:

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

最佳答案

引用 Vladimir Kozlov 在 hotspot-compiler-dev 提供的权威答案邮件列表:

Hi Marko,

For primitive arrays we use handwritten assembler code which use XMM registers as vectors for initialization. For object arrays we did not optimize it because it is not common case. We can improve it similar to what we did for arracopy but we decided leave it for now.

Regards,
Vladimir

我也想知道为什么优化后的代码没有内联,也得到了答案:

The code is not small, so we decided to not inline it. Look on MacroAssembler::generate_fill() in macroAssembler_x86.cpp:

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 从相同的优化想法中受益。

关于java - 什么可以解释写入对堆位置的引用的巨大性能损失?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21523958/

相关文章:

java - 将 SecureRandom 与 SHA-256 结合使用

c# - 使用 GC.AddMemoryPressure 触发更频繁的运行时可调用包装器 (RCW) 终结是否合适?

scala 特化 - 使用对象而不是类会导致速度减慢?

java - 在 Java : Is my benchmarking code wrong? 中对小型数组与列表进行基准测试

java - Iterator 或 foreach 的延迟

java - 对话范围是否泄漏?

java - 什么是时间复杂度 O(1) 次,Java 中的 O(n) 次

java - 此 Java 代码中有多少对象符合垃圾回收条件?

php - 即使 session.gc_probability 设置为 0,Symfony 也会在 Ubuntu 14.04 上调用 PHP 垃圾收集器

linux - 当使用物理和逻辑内核的差异号时,在哪里可以找到英特尔处理器(比如 skylake)的 ipc(或 cpi)值?