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上找到。