System.arraycopy与固定长度

14

我正在尝试使用JMH进行实验 (http://openjdk.java.net/projects/code-tools/jmh/),偶然间遇到了一个奇怪的结果。

我正在测试对数组进行浅拷贝的方法,并且我可以观察到预期的结果(通过循环遍历数组是一个不好的想法,而在性能方面,在#clone()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

还有基准测试类:

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class ArrayCopyBenchmark {

    private static final int LENGTH = 32;

    private Object[] array;

    @Setup
    public void before() {
        array = new Object[LENGTH];
        for (int i = 0; i < LENGTH; i++) {
            array[i] = new Object();
        }
    }

    @Benchmark
    public Object[] Clone() {
        Object[] src = this.array;
        return src.clone();
    }

    @Benchmark
    public Object[] ArraysCopyOf() {
        Object[] src = this.array;
        return Arrays.copyOf(src, src.length);
    }

    @Benchmark
    public Object[] ArraysCopyOf_ConstantSize() {
        Object[] src = this.array;
        return Arrays.copyOf(src, LENGTH);
    }

    @Benchmark
    public Object[] ArraysCopyOf_Class() {
        Object[] src = this.array;
        return Arrays.copyOf(src, src.length, Object[].class);
    }

    @Benchmark
    public Object[] ArraysCopyOf_Class_ConstantSize() {
        Object[] src = this.array;
        return Arrays.copyOf(src, LENGTH, Object[].class);
    }

    @Benchmark
    public Object[] SystemArrayCopy() {
        Object[] src = this.array;
        int length = src.length;
        Object[] array = new Object[length];
        System.arraycopy(src, 0, array, 0, length);
        return array;
    }

    @Benchmark
    public Object[] SystemArrayCopy_ConstantSize() {
        Object[] src = this.array;
        Object[] array = new Object[LENGTH];
        System.arraycopy(src, 0, array, 0, LENGTH);
        return array;
    }

    @Benchmark
    public Object[] Loop() {
        Object[] src = this.array;
        int length = src.length;
        Object[] array = new Object[length];
        for (int i = 0; i < length; i++) {
            array[i] = src[i];
        }
        return array;
    }

    @Benchmark
    public Object[] Loop_ConstantSize() {
        Object[] src = this.array;
        Object[] array = new Object[LENGTH];
        for (int i = 0; i < LENGTH; i++) {
            array[i] = src[i];
        }
        return array;
    }
}

1
奇怪,我以为Arrays.copyof内部调用System.arraycopy,这意味着它应该根据定义具有较低的吞吐量,你尝试运行这个基准测试多少次了? - MahdeTo
2
尝试增加您的热身时间。20不足以启动JIT。 - Andreas
@MahdeTo 5次。结果没有太大变化。 - omiel
1
@Andreas 这些是JMH的默认设置。它包括20次1秒的迭代,每次迭代会产生(根据结果判断)数千万次的调用。 - omiel
我尝试改变基准测试运行的顺序,将JVM更新到更高版本(1.8.0_60),除了System#arraycopy()之外,删除所有基准测试。但是我仍然得到相同的结果。 - omiel
显示剩余2条评论
1个回答

10

通常,这种类型的问题可以通过研究生成的代码来快速解答。在Linux上,JMH为您提供了-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存储操作,但少执行一次LLC加载。嗯,这就是我们在常量情况下需要寻找的,更多的存储操作。 -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 查看,并可通过 -prof perfnorm 进行确认:

Benchmark                                     Mode  Cnt    Score    Error  Units
ACB.SAC                                       avgt   50   14.156 ±  0.492  ns/op
ACB.SAC:·CPI                                  avgt    5    0.612 ±  0.144   #/op
ACB.SAC:·L1-dcache-load-misses                avgt    5    2.363 ±  0.341   #/op
ACB.SAC:·L1-dcache-loads                      avgt    5   28.350 ±  2.181   #/op
ACB.SAC:·L1-dcache-store-misses               avgt    5    2.287 ±  0.607   #/op
ACB.SAC:·L1-dcache-stores                     avgt    5   16.922 ±  3.402   #/op
ACB.SAC:·branches                             avgt    5   21.242 ±  5.914   #/op
ACB.SAC:·cycles                               avgt    5   67.168 ± 20.950   #/op
ACB.SAC:·instructions                         avgt    5  109.931 ± 35.905   #/op

ACB.SAC_ConstantSize                          avgt   50   13.763 ±  0.067  ns/op
ACB.SAC_ConstantSize:·CPI                     avgt    5    0.625 ±  0.024   #/op
ACB.SAC_ConstantSize:·L1-dcache-load-misses   avgt    5    2.376 ±  0.214   #/op
ACB.SAC_ConstantSize:·L1-dcache-loads         avgt    5   28.285 ±  2.127   #/op
ACB.SAC_ConstantSize:·L1-dcache-store-misses  avgt    5    2.335 ±  0.223   #/op
ACB.SAC_ConstantSize:·L1-dcache-stores        avgt    5   16.926 ±  1.467   #/op
ACB.SAC_ConstantSize:·branches                avgt    5   19.469 ±  0.869   #/op
ACB.SAC_ConstantSize:·cycles                  avgt    5   62.395 ±  3.898   #/op
ACB.SAC_ConstantSize:·instructions            avgt    5   99.891 ±  5.435   #/op

当然,所有这些关于arraycopy的纳秒基准测试都容易受到矢量化复制存根中奇怪的对齐引起的性能差异的影响,但那是另一个(恐怖)故事,我没有勇气讲述。


1
感谢您提供这个很棒的答案。我怀疑JVM无法推断我正在使用数组的全部长度,但我不知道如何证明它。 - omiel
我尝试复制您的结果,但我认为perfasm/xperfasm在Mac OSX上不可用。 - omiel
是的,在Linux上可以使用perfasm,在Windows上可以使用xperfasm。如果有一种方法可以获取Mac OS X上程序地址热点数据,我们也可以在那里实现相同的功能。欢迎提交补丁。 - Aleksey Shipilev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接