Performance of System.arraycopy() vs. Arrays.copyOf() – System.arraycopy()与Arrays.copyOf()的性能对比

最后修改: 2021年 9月 14日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we will look at the performance of two Java methods: System.arraycopy() and Arrays.copyOf(). First, we’ll analyze their implementations. Second, we’ll run some benchmarks to compare their average execution times.

在本教程中,我们将研究两个Java方法的性能。System.arraycopy()Arrays.copyOf()。首先,我们将分析它们的实现。其次,我们将运行一些基准测试来比较它们的平均执行时间。

2. Performance of System.arraycopy()

2.System.arraycopy()的性能

System.arraycopy() copies the array contents from the source array, beginning at the specified position, to the designated position in the destination array. Additionally, before copying, the JVM checks that both source and destination types are the same.

System.arraycopy()从源数组的指定位置开始,将数组内容复制到目标数组的指定位置。此外,在复制之前,JVM会检查源和目的类型是否相同。

When estimating the performance of System.arraycopy(), we need to keep in mind that it is a native method. Native methods are implemented in platform-dependent code (typically C) and accessed through JNI calls.

在估算System.arraycopy()的性能时,我们需要记住它是一个本地方法。本地方法是在依赖于平台的代码(通常是C)中实现的,并通过JNI调用进行访问。

Because native methods are already compiled for a specific architecture, we can’t precisely estimate the runtime complexity. Moreover, their complexities can differ between platforms. We can be sure that the worst-case scenario is O(N). However, the processor can copy contiguous blocks of memory one block at a time (memcpy() in C), so actual results can be better.

因为本地方法已经针对特定的架构进行了编译,所以我们无法精确估计运行时的复杂性。此外,它们的复杂度在不同的平台上也会有所不同。我们可以肯定,最坏的情况是O(N)。然而,处理器可以一次复制连续的内存块(C语言中的memcpy()),所以实际结果可能更好。

We can view only the signature of System.arraycopy():

我们只能查看System.arraycopy()的签名。

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

3. Performance of Arrays.copyOf()

3.Arrays.copyOf()的性能

Arrays.copyOf() offers additional functionality on top of what System.arraycopy() implements. While System.arraycopy() simply copies values from the source array to the destination, Arrays.copyOf() also creates new array. If necessary, it will truncate or pad the content.

Arrays.copyOf()System.arraycopy()实现的基础上提供了额外的功能。虽然System.arraycopy()只是简单地将源数组中的值复制到目标数组中,但Arrays.copyOf()也会创建新的数组。如果有必要,它将截断或填充内容。

The second difference is that the new array can be of a different type than the source array. If that’s the case, the JVM will use reflection, which adds performance overhead.

第二个区别是,新数组的类型可能与源数组不同。如果是这种情况,JVM将使用反射,这将增加性能开销

When called with an Object array, copyOf() will invoke the reflective Array.newInstance() method:

当用一个Object数组调用时,copyOf()将调用反射性的Array.newInstance()方法。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class) 
      ? (T[]) new Object[newLength]
      : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

However, when invoked with primitives as parameters, it doesn’t need reflection to create a destination array:

然而,当用基元作为参数调用时,它不需要反射来创建一个目标数组。

public static int[] copyOf(int[] original, int newLength) {
    int[] copy = new int[newLength];
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

We can clearly see that currently, the implementation of Arrays.copyOf() calls System.arraycopy(). As a result, runtime execution should be similar. To confirm our suspicion, we will benchmark the above methods with both primitives and objects as parameters.

我们可以清楚地看到,目前,Arrays.copyOf()的实现调用System.arraycopy()。因此,运行时的执行情况应该是相似的。为了证实我们的猜测,我们将用基元和对象作为参数对上述方法进行基准测试。

4. Code Benchmark

4.代码基准

Let’s check which copy method is faster with the real test. To do that, we’ll use JMH (Java Microbenchmark Harness). We’ll create a simple test in which we will copy values from one array to the other using both System.arraycopy() and Arrays.copyOf().

让我们通过实际测试来检查哪种复制方法更快。为此,我们将使用JMH(Java Microbenchmark Harness)。我们将创建一个简单的测试,其中我们将使用System.arraycopy()Arrays.copyOf()将数值从一个数组复制到另一个数组。

We’ll create two test classes. In one test class, we will test primitives, and in the second, we’ll test objects. The benchmark configuration will be the same in both cases.

我们将创建两个测试类。在一个测试类中,我们将测试基元,而在第二个测试类中,我们将测试对象。两种情况下的基准配置将是相同的。

4.1. Benchmark Configuration

4.1.基准配置

First, let’s define our benchmark parameters:

首先,让我们定义我们的基准参数。

@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 10)
@Fork(1)
@Measurement(iterations = 100)

Here, we specify that we want to run our benchmark only once, with 10 warmup iterations and 100 measurement iterations.  Moreover, we would like to calculate the average execution time and collect the results in nanoseconds. To obtain exact results, it is important to perform at least five warmup iterations.

在这里,我们指定我们的基准只运行一次,有10次热身迭代和100次测量迭代。 此外,我们想计算平均执行时间,并收集纳秒级的结果。为了获得准确的结果,必须至少进行五次预热迭代。

4.2. Parameters Setup

4.2.参数设置

We need to be sure that we measure only the time spent on method execution and not on array creation. To do that, we’ll initialize the source array in the benchmark setup phase. It’s a good idea to run the benchmark with both big and small numbers.

我们需要确保我们只测量方法执行的时间,而不是数组创建的时间。为了做到这一点,我们将在基准设置阶段初始化源数组。用大数字和小数字运行基准是个好主意。

In the setup method, we simply initialize an array with random parameters. First, we define the benchmark setup for primitives:

在设置方法中,我们只是用随机参数初始化一个数组。首先,我们定义基元的基准设置。

public class PrimitivesCopyBenchmark {

    @Param({ "10", "1000000" })
    public int SIZE;

    int[] src;

    @Setup
    public void setup() {
        Random r = new Random();
        src = new int[SIZE];

        for (int i = 0; i < SIZE; i++) {
            src[i] = r.nextInt();
        }
    }
}

The same setup follows for the objects benchmark:

同样的设置也适用于对象基准。

public class ObjectsCopyBenchmark {

    @Param({ "10", "1000000" })
    public int SIZE;
    Integer[] src;

    @Setup
    public void setup() {
        Random r = new Random();
        src = new Integer[SIZE];

        for (int i = 0; i < SIZE; i++) {
            src[i] = r.nextInt();
        }
    }
}

4.3. Tests

4.3 测试

We define two benchmarks that will execute copy operations. First, we’ll call System.arraycopy():

我们定义了两个将执行复制操作的基准。首先,我们将调用System.arraycopy()

@Benchmark
public Integer[] systemArrayCopyBenchmark() {
    Integer[] target = new Integer[SIZE];
    System.arraycopy(src, 0, target, 0, SIZE);
    return target;
}

To make both tests equivalent, we’ve included target array creation in the benchmark.

为了使两个测试相当,我们在基准中加入了目标阵列的创建。

Second, we’ll measure the performance of Arrays.copyOf():

第二,我们将测量Arrays.copyOf()的性能。

@Benchmark
public Integer[] arraysCopyOfBenchmark() {
    return Arrays.copyOf(src, SIZE);
}

4.4. Results

4.4.结果

After running our test, let’s look at the results:

运行我们的测试后,让我们看看结果。

Benchmark                                          (SIZE)  Mode  Cnt        Score       Error  Units
ObjectsCopyBenchmark.arraysCopyOfBenchmark             10  avgt  100        8.535 ±     0.006  ns/op
ObjectsCopyBenchmark.arraysCopyOfBenchmark        1000000  avgt  100  2831316.981 ± 15956.082  ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark          10  avgt  100        9.278 ±     0.005  ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark     1000000  avgt  100  2826917.513 ± 15585.400  ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark          10  avgt  100        9.172 ±     0.008  ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark     1000000  avgt  100   476395.127 ±   310.189  ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark       10  avgt  100        8.952 ±     0.004  ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark  1000000  avgt  100   475088.291 ±   726.416  ns/op

As we can see, the performance of System.arraycopy() and Arrays.copyOf() differs on the range of measurement error for both primitives and Integer objects. It isn’t surprising, considering the fact that Arrays.copyOf() uses System.arraycopy() under the hood. Since we used two primitive int arrays, no reflective calls were made.

我们可以看到,System.arraycopy()Arrays.copyOf()的性能在基元和Integer对象的测量误差范围内有所不同。这并不奇怪,考虑到Arrays.copyOf()在幕后使用了System.arraycopy()的事实。由于我们使用了两个原始的int数组,所以没有进行反射性调用。

We need to remember that JMH gives just a rough estimation of execution times, and the results can differ between machines and JVMs.

我们需要记住,JMH给出的只是对执行时间的粗略估计,而且不同的机器和JVM的结果可能不同。

5. Intrinsic Candidates

5.本质上的候选者

It’s worth noting that in HotSpot JVM 16, both Arrays.copyOf() and System.arraycopy() are marked as @IntrinsicCandidate. This annotation means that the annotated method can be replaced with faster low-level code by the HotSpot VM.

值得注意的是,在HotSpot JVM 16中,Arrays.copyOf()System.arraycopy()都被标记为@IntrinsicCandidate。这个注解意味着被注解的方法可以被HotSpot虚拟机用更快的低级代码取代。

The JIT compiler can (for some or all architectures) substitute intrinsic methods with machine-dependent, greatly optimized instructions. Since native methods are a black box to the compiler, with significant call overhead, the performance of both methods can be better. Again, such performance gains aren’t guaranteed.

JIT编译器可以(对于某些或所有的架构)用依赖于机器的、大大优化的指令来替代本机方法。由于本机方法对编译器来说是一个黑匣子,有很大的调用开销,所以两种方法的性能都可以更好。同样,这样的性能提升也是不能保证的。

6. Conclusion

6.结语

In this example, we’ve looked into the performance of System.arraycopy() and Arrays.copyOf(). First, we analyzed the source code of both methods. Second, we set up an example benchmark to measure their average execution times.

在这个例子中,我们研究了System.arraycopy()和Arrays.copyOf()的性能。首先,我们分析了两种方法的源代码。其次,我们设置了一个实例基准来测量它们的平均执行时间。

As a result, we have confirmed our theory that because Arrays.copyOf() uses System.arraycopy(), the performance of both methods is very similar.

结果,我们证实了我们的理论:由于Arrays.copyOf()使用了System.arraycopy(),两种方法的性能非常相似。

As usual, the examples used in this article are available over on GitHub.

像往常一样,本文中使用的例子可以在GitHub上找到