1. 引言

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

2. System.arraycopy() 的性能

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

在评估System.arraycopy() 的性能时,我们需要记住它是一个原生方法。 原生方法是在平台依赖的代码(通常使用C)中实现并通过JNI调用的。

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

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

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

3. Arrays.copyOf() 的性能

Arrays.copyOf()System.arraycopy()的功能之上提供了额外的功能。尽管System.arraycopy()只是简单地从源数组复制值到目标,但Arrays.copyOf()还会创建新的数组。如果需要,它会截断或填充内容。

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

当使用对象数组调用时,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;
}

但是,当使用基本类型作为参数时,它不需要反射来创建目标数组:

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

我们可以清楚地看到,**当前Arrays.copyOf()的实现调用了System.arraycopy()**。因此,运行时执行应该相似。为了验证我们的怀疑,我们将使用基本类型和对象作为参数对上述方法进行基准测试。

4. 代码基准测试

现在让我们通过实际测试来看看哪个复制方法更快。为此,我们将使用JMH(Java微基准测试框架)。我们将创建一个简单的测试,使用System.arraycopy()Arrays.copyOf() 来复制数组值。

我们将创建两个测试类。在一个测试类中,我们将测试基本类型,而在另一个测试类中,我们将测试对象。基准配置在两种情况下都是一样的。

4.1. 测试配置

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

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

在这里,我们指定只运行一次基准测试,有10次预热迭代和100次测量迭代。此外,我们希望计算平均执行时间和以纳秒为单位收集结果。为了获得准确的结果,至少需要进行五次预热迭代。

4.2. 参数设置

我们需要确保我们只测量方法执行的时间,而不是数组的创建时间。为此,我们将在基准设置阶段初始化源数组。最好用大数和小数运行基准测试。

在设置方法中,我们简单地使用随机参数初始化数组。首先,我们定义基本类型的基准设置:

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();
        }
    }
}

对象基准的设置与此类似:

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. 测试

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

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

为了使两个测试等效,我们在基准中包含了目标数组的创建。

其次,我们将衡量Arrays.copyOf()的性能:

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

4.4. 结果

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

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

正如我们所见,对于基本类型和整型对象,System.arraycopy()Arrays.copyOf() 的性能在测量误差范围内有所不同。考虑到Arrays.copyOf()在底层使用System.arraycopy()的事实,这并不奇怪。因为我们使用的是两个基本类型int数组,所以没有进行反射调用。

需要注意的是,JMH给出的是对执行时间的粗略估计,结果可能在不同的机器和JVM上有所不同。

5. 内置候选方法

值得一提的是,在HotSpot JVM 16中,Arrays.copyOf()System.arraycopy() 都被标记为@IntrinsicCandidate。这个注解意味着,这些被标记的方法可以被HotSpot VM替换为更快的底层代码。

JIT编译器可以(对于某些或所有架构)用机器依赖的、高度优化的指令替换内置方法。由于原生方法对编译器来说是一个黑盒,具有显著的调用开销,这两种方法的性能可能会更好。然而,这样的性能提升并不保证。

6. 总结

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

结果显示,正如我们理论预测的那样,因为Arrays.copyOf()使用了System.arraycopy(),所以两种方法的性能非常相似。

如往常一样,本文中的示例代码可以在GitHub上找到。


« 上一篇: Java Weekly, 第403期