为什么对于小数组,Arrays.copyOf比System.arraycopy快2倍?

11

我最近在进行一些基准测试,发现了一些非常有趣的结果,但目前无法解释。以下是该基准测试:

@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, batchSize = 1000)
@Measurement(iterations = 10, time = 1, batchSize = 1000)
public class ArrayCopy {

    @Param({"1","5","10","100", "1000"})
    private int size;
    private int[] ar;

    @Setup
    public void setup() {
        ar = new int[size];
        for (int i = 0; i < size; i++) {
            ar[i] = i;
        }
    }

    @Benchmark
    public int[] SystemArrayCopy() {
        final int length = size;
        int[] result = new int[length];
        System.arraycopy(ar, 0, result, 0, length);
        return result;
    }

    @Benchmark
    public int[] javaArrayCopy() {
        final int length = size;
        int[] result = new int[length];
        for (int i = 0; i < length; i++) {
            result[i] = ar[i];
        }
        return result;
    }

    @Benchmark
    public int[] arraysCopyOf() {
        final int length = size;
        return Arrays.copyOf(ar, length);
    }

}

结果:

Benchmark                  (size)   Mode  Cnt       Score      Error  Units
ArrayCopy.SystemArrayCopy       1  thrpt   10   52533.503 ± 2938.553  ops/s
ArrayCopy.SystemArrayCopy       5  thrpt   10   52518.875 ± 4973.229  ops/s
ArrayCopy.SystemArrayCopy      10  thrpt   10   53527.400 ± 4291.669  ops/s
ArrayCopy.SystemArrayCopy     100  thrpt   10   18948.334 ±  929.156  ops/s
ArrayCopy.SystemArrayCopy    1000  thrpt   10    2782.739 ±  184.484  ops/s
ArrayCopy.arraysCopyOf          1  thrpt   10  111665.763 ± 8928.007  ops/s
ArrayCopy.arraysCopyOf          5  thrpt   10   97358.978 ± 5457.597  ops/s
ArrayCopy.arraysCopyOf         10  thrpt   10   93523.975 ± 9282.989  ops/s
ArrayCopy.arraysCopyOf        100  thrpt   10   19716.960 ±  728.051  ops/s
ArrayCopy.arraysCopyOf       1000  thrpt   10    1897.061 ±  242.788  ops/s
ArrayCopy.javaArrayCopy         1  thrpt   10   58053.872 ± 4955.749  ops/s
ArrayCopy.javaArrayCopy         5  thrpt   10   49708.647 ± 3579.826  ops/s
ArrayCopy.javaArrayCopy        10  thrpt   10   48111.857 ± 4603.024  ops/s
ArrayCopy.javaArrayCopy       100  thrpt   10   18768.866 ±  445.238  ops/s
ArrayCopy.javaArrayCopy      1000  thrpt   10    2462.207 ±  126.549  ops/s
这里有两个奇怪的事情:
  • Arrays.copyOf 在小数组(1,5,10大小)中比 System.arraycopy 快2倍。然而,在大小为1000的大数组上,Arrays.copyOf 几乎慢2倍。我知道这两种方法都是内置函数,所以我期望它们具有相同的性能。这个差异从哪里来?
  • 对于一个1元素数组,手动复制要比 System.arraycopy 快。我不清楚这是为什么。有人知道吗?

虚拟机版本: JDK 1.8.0_131, VM 25.131-b11


2
由于copyOf内部使用了arraycopy,所以你的基准测试有问题。 - Andreas
3
@Andreas,你说得不对。Arrays.copyOf 是 JVM 内置方法。一旦该方法被 JIT 编译,Arrays.java 中的 Java 代码就不会被执行。 - apangin
@Andreas,你认为这个基准测试有什么问题吗?它明智地使用JMH框架来避免常见的基准测试陷阱。 - apangin
@apangin 这是没有区别的区分。对于任何Java代码都适用。这并不意味着任何随意的方法都是“JVM内在的”。 - user207421
5
JIT编译器不会查看Arrays.copyOf的字节码,因为它内部已经知道这个方法应该做什么。我可以为您复述一下:JIT编译器不需要查看Arrays.copyOf的字节码,因为它已经内置了对该方法的理解。 - apangin
1个回答

8
您的System.arraycopy基准测试与Arrays.copyOf在语义上不等价。

如果您替换为以下内容,则会等价:

    System.arraycopy(ar, 0, result, 0, length);

使用

    System.arraycopy(ar, 0, result, 0, Math.min(ar.length, length));

随着这个变化,两个基准测试的性能将变得相似。为什么第一个变体比较慢呢?
  1. Without knowing how length relates to ar.length JVM needs to perform additional bounds check and be prepared to throw IndexOutOfBoundsException when length > ar.length.

  2. This also breaks the optimization to eliminate redundant zeroing. You know, every allocated array must be initialized with zeros. However, JIT can avoid zeroing if it sees that the array is filled right after creation. But -prof perfasm clearly shows that the original System.arraycopy benchmark spends a significant amount of time clearing the allocated array:

     0,84%    0x000000000365d35f: shr    $0x3,%rcx
     0,06%    0x000000000365d363: add    $0xfffffffffffffffe,%rcx
     0,69%    0x000000000365d367: xor    %rax,%rax
              0x000000000365d36a: shl    $0x3,%rcx
    21,02%    0x000000000365d36e: rep rex.W stos %al,%es:(%rdi)  ;*newarray
    
手动复制对于小数组来说速度更快,因为它不像 System.arraycopy 那样执行任何运行时调用 VM 函数的操作。

1
ar.lengthlength 不是一样的吗? Math.min(ar.length, length) 有什么影响呢? - Boann
5
@EJP,我不确定你指的是哪个编译器,但HotSpot JIT编译器(实际上是C1和C2)肯定知道 System.arraycopy() 并通过将其调用转换为IR节点图翻译。然后,数据流分析有助于缩小此图。 - apangin
@apangin 谢谢!你知道为什么 copyOf 在更大的数组上变得更慢吗? - Dmitriy Dumanskiy
有趣。我会再次检查。 - Dmitriy Dumanskiy
1
@EJP 谁让这个术语消失了?'JIT'仍然在HotSpot网站bug跟踪器OpenJDK邮件列表和由Oracle工程师在公开演示中正式使用。 - apangin
显示剩余5条评论

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