具有恒定长度的System.arraycopy

时间:2015-09-29 02:45:52

标签: java arrays microbenchmark jmh

我正在玩JMH(http://openjdk.java.net/projects/code-tools/jmh/)而我偶然发现了一个奇怪的结果。

我正在对制作数组的浅层副本的方法进行基准测试,我可以观察到预期的结果(循环遍历数组是一个坏主意,并且#clone()之间没有显着差异,{{1并且System#arraycopy(),表现明智的。)

当数组的长度是硬编码时,Arrays#copyOf()减慢四分之一......等等,什么?怎么会这么慢?

有没有人知道可能是什么原因?

结果(吞吐量):

System#arraycopy()

基准类:

# JMH 1.11 (released 17 days ago)
# VM version: JDK 1.8.0_05, VM 25.5-b02
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8 -Duser.country=FR -Duser.language=fr -Duser.variant
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time

Benchmark                                            Mode  Cnt         Score         Error  Units
ArrayCopyBenchmark.ArraysCopyOf                     thrpt   20  67100500,319 ±  455252,537  ops/s
ArrayCopyBenchmark.ArraysCopyOf_Class               thrpt   20  65246374,290 ±  976481,330  ops/s
ArrayCopyBenchmark.ArraysCopyOf_Class_ConstantSize  thrpt   20  65068143,162 ± 1597390,531  ops/s
ArrayCopyBenchmark.ArraysCopyOf_ConstantSize        thrpt   20  64463603,462 ±  953946,811  ops/s
ArrayCopyBenchmark.Clone                            thrpt   20  64837239,393 ±  834353,404  ops/s
ArrayCopyBenchmark.Loop                             thrpt   20  21070422,097 ±  112595,764  ops/s
ArrayCopyBenchmark.Loop_ConstantSize                thrpt   20  24458867,274 ±  181486,291  ops/s
ArrayCopyBenchmark.SystemArrayCopy                  thrpt   20  66688368,490 ±  582416,954  ops/s
ArrayCopyBenchmark.SystemArrayCopy_ConstantSize     thrpt   20  48992312,357 ±  298807,039  ops/s

1 个答案:

答案 0 :(得分:10)

像往常一样,通过研究生成的代码可以快速回答这些问题。 JMH在Linux上为您提供-prof perfasm,在Windows上为您提供-prof xperfasm。如果您在JDK 8u40上运行基准测试,那么您将看到(注意我使用-bm avgt -tu ns来使分数更易于理解):

Benchmark                         Mode  Cnt   Score   Error  Units
ACB.SystemArrayCopy               avgt   25  13.294 ± 0.052  ns/op
ACB.SystemArrayCopy_ConstantSize  avgt   25  16.413 ± 0.080  ns/op

为什么这些基准测试表现不同?让我们先做-prof perfnorm解剖(我放弃了无关紧要的行):

Benchmark                                     Mode  Cnt    Score    Error  Units
ACB.SAC                                       avgt   25   13.466 ±  0.070  ns/op
ACB.SAC:·CPI                                  avgt    5    0.602 ±  0.025   #/op
ACB.SAC:·L1-dcache-load-misses                avgt    5    2.346 ±  0.239   #/op
ACB.SAC:·L1-dcache-loads                      avgt    5   24.756 ±  1.438   #/op
ACB.SAC:·L1-dcache-store-misses               avgt    5    2.404 ±  0.129   #/op
ACB.SAC:·L1-dcache-stores                     avgt    5   14.929 ±  0.230   #/op
ACB.SAC:·LLC-loads                            avgt    5    2.151 ±  0.217   #/op
ACB.SAC:·branches                             avgt    5   17.795 ±  1.003   #/op
ACB.SAC:·cycles                               avgt    5   56.677 ±  3.187   #/op
ACB.SAC:·instructions                         avgt    5   94.145 ±  6.442   #/op

ACB.SAC_ConstantSize                          avgt   25   16.447 ±  0.084  ns/op
ACB.SAC_ConstantSize:·CPI                     avgt    5    0.637 ±  0.016   #/op
ACB.SAC_ConstantSize:·L1-dcache-load-misses   avgt    5    2.357 ±  0.206   #/op
ACB.SAC_ConstantSize:·L1-dcache-loads         avgt    5   25.611 ±  1.482   #/op
ACB.SAC_ConstantSize:·L1-dcache-store-misses  avgt    5    2.368 ±  0.123   #/op
ACB.SAC_ConstantSize:·L1-dcache-stores        avgt    5   25.593 ±  1.610   #/op
ACB.SAC_ConstantSize:·LLC-loads               avgt    5    1.050 ±  0.038   #/op
ACB.SAC_ConstantSize:·branches                avgt    5   17.853 ±  0.697   #/op
ACB.SAC_ConstantSize:·cycles                  avgt    5   66.680 ±  2.049   #/op
ACB.SAC_ConstantSize:·instructions            avgt    5  104.759 ±  4.831   #/op

所以,ConstantSize以某种方式做了更多L1-dcache-store,但少了一次LLC-load。嗯,这就是我们正在寻找的,在不变的情况下更多的商店。 -prof perfasm可以方便地突出显示汇编中的热点部分:

default

  4.32%    6.36%   0x00007f7714bda2dc: movq   $0x1,(%rax)            ; alloc
  0.09%    0.04%   0x00007f7714bda2e3: prefetchnta 0x100(%r9)
  2.95%    1.48%   0x00007f7714bda2eb: movl   $0xf80022a9,0x8(%rax)
  0.38%    0.18%   0x00007f7714bda2f2: mov    %r11d,0xc(%rax)
  1.56%    3.02%   0x00007f7714bda2f6: prefetchnta 0x140(%r9)
  4.73%    2.71%   0x00007f7714bda2fe: prefetchnta 0x180(%r9)

ConstantSize

  0.58%    1.22%   0x00007facf921132b: movq   $0x1,(%r14)            ; alloc
  0.84%    0.72%   0x00007facf9211332: prefetchnta 0xc0(%r10)
  0.11%    0.13%   0x00007facf921133a: movl   $0xf80022a9,0x8(%r14)
  0.21%    0.68%   0x00007facf9211342: prefetchnta 0x100(%r10)
  0.50%    0.87%   0x00007facf921134a: movl   $0x20,0xc(%r14)
  0.53%    0.82%   0x00007facf9211352: mov    $0x10,%ecx
  0.04%    0.14%   0x00007facf9211357: xor    %rax,%rax
  0.34%    0.76%   0x00007facf921135a: shl    $0x3,%rcx
  0.50%    1.17%   0x00007facf921135e: rex.W rep stos %al,%es:(%rdi) ; zeroing
 29.49%   52.09%   0x00007facf9211361: prefetchnta 0x140(%r10)
  1.03%    0.53%   0x00007facf9211369: prefetchnta 0x180(%r10)  

所以有一个令人讨厌的rex.W rep stos %al,%es:(%rdi)消耗了很长时间。这会将新分配的数组归零。在ConstantSize测试中,JVM无法关联您覆盖整个目标数组,因此在深入实际数组副本之前必须将其预先归零。

如果您查看JDK 9b82上生成的代码(最新的可用代码),那么您将看到它将非归零副本中的两种模式折叠起来,您可以使用-prof perfasm查看,也可以使用{ {1}}:

-prof perfnorm

当然,所有这些用于阵列检查的纳米标记都容易受到矢量化复制存根中奇怪的对齐引起的性能差异的影响,但这是另一个(恐怖)故事,我没有勇气说出来。