Java的System.arraycopy()
方法对于小数组来说是否高效,还是因为它是本地方法而可能比简单循环和函数调用方式效率明显低?
本地方法是否会因为跨越某种Java系统桥梁而产生额外的性能开销?
Java的System.arraycopy()
方法对于小数组来说是否高效,还是因为它是本地方法而可能比简单循环和函数调用方式效率明显低?
本地方法是否会因为跨越某种Java系统桥梁而产生额外的性能开销?
System.arraycopy
只是一个JIT内置函数;这意味着当代码调用System.arraycopy
时,它很可能会调用一个JIT特定的实现(一旦JIT将System.arraycopy
标记为“热点”),该实现不通过JNI接口执行,因此不会产生本地方法的常规开销。这是我的基准测试代码:
public void test(int copySize, int copyCount, int testRep) {
System.out.println("Copy size = " + copySize);
System.out.println("Copy count = " + copyCount);
System.out.println();
for (int i = testRep; i > 0; --i) {
copy(copySize, copyCount);
loop(copySize, copyCount);
}
System.out.println();
}
public void copy(int copySize, int copyCount) {
int[] src = newSrc(copySize + 1);
int[] dst = new int[copySize + 1];
long begin = System.nanoTime();
for (int count = copyCount; count > 0; --count) {
System.arraycopy(src, 1, dst, 0, copySize);
dst[copySize] = src[copySize] + 1;
System.arraycopy(dst, 0, src, 0, copySize);
src[copySize] = dst[copySize];
}
long end = System.nanoTime();
System.out.println("Arraycopy: " + (end - begin) / 1e9 + " s");
}
public void loop(int copySize, int copyCount) {
int[] src = newSrc(copySize + 1);
int[] dst = new int[copySize + 1];
long begin = System.nanoTime();
for (int count = copyCount; count > 0; --count) {
for (int i = copySize - 1; i >= 0; --i) {
dst[i] = src[i + 1];
}
dst[copySize] = src[copySize] + 1;
for (int i = copySize - 1; i >= 0; --i) {
src[i] = dst[i];
}
src[copySize] = dst[copySize];
}
long end = System.nanoTime();
System.out.println("Man. loop: " + (end - begin) / 1e9 + " s");
}
public int[] newSrc(int arraySize) {
int[] src = new int[arraySize];
for (int i = arraySize - 1; i >= 0; --i) {
src[i] = i;
}
return src;
}
根据我的测试,调用test()
并将copyCount
设为10000000(1e7)或更高,可以在第一个copy/loop
调用期间实现预热效果,因此使用testRep
=5就足够了;当copyCount
为1000000(1e6)时,需要至少2到3次迭代来达到预热效果,因此需要增加testRep
以获得可用的结果。copySize
=24时,System.arraycopy()
和手动循环所需时间几乎相同(有时其中一个略微快于另一个,有时则相反),copySize
<24时,手动循环比System.arraycopy()
更快(copySize
=23时略微快,copySize
<5时则更快),copySize
>24时,System.arraycopy()
比手动循环更快(copySize
=25时略微快,随着copySize
的增加,循环时间/数组拷贝时间的比例也会增加)。这是一个合理的担忧。例如,在 java.nio.DirectByteBuffer.put(byte[])
方法中,作者试图避免在处理少量元素时进行JNI拷贝。
// These numbers represent the point at which we have empirically
// determined that the average cost of a JNI call exceeds the expense
// of an element by element copy. These numbers may change over time.
static final int JNI_COPY_TO_ARRAY_THRESHOLD = 6;
static final int JNI_COPY_FROM_ARRAY_THRESHOLD = 6;
对于System.arraycopy()
,我们可以看一下JDK如何使用它。例如,在ArrayList
中,无论长度(即使为0),始终使用System.arraycopy()
而不是 "逐个元素复制"。由于ArrayList
非常注重性能,因此我们可以得出结论,System.arraycopy()
是无论长度如何都最高效的数组复制方式。
System.arraycopy()
是否完全通过JNI。正如有人指出的那样,仅仅因为它被声明为“native”并不意味着任何事情,因为JVM允许具有各种特殊优化。 - GravityArrayList
类也有同样的想法,即使在我提出这个问题时也是如此:ArrayList
被认为是一个非常优化的类,不会执行任何不必要的昂贵操作。但是,也许作者们认为小型ArrayLists
的性能并不那么重要(因为最昂贵的操作通常是在大型数据集上进行的),或者它在所有JVM中的效率都不一致。我需要进行基准测试才能信服。 - Gravity我没有依赖猜测和可能过时的信息,而是使用caliper运行了一些基准测试。实际上,Caliper带有一些示例,包括一个CopyArrayBenchmark
,可以精确地测量这个问题!你只需要运行它。
mvn exec:java -Dexec.mainClass=com.google.caliper.runner.CaliperMain -Dexec.args=examples.CopyArrayBenchmark
for
循环以将每个元素复制到新实例化的数组中从来不是优势,即使对于仅含5个元素的数组也是如此。Arrays.copyOf(array, array.length)
和 array.clone()
都具有一致的快速性。这两种技术在性能上几乎相同;选择哪种是个人口味问题。System.arraycopy(src, 0, dest, 0, src.length)
的速度几乎与 Arrays.copyOf(array, array.length)
和 array.clone()
相当,但并不总是如此。(请参见50000个 int
的情况。)因此,由于调用的冗长和需要对要复制的元素进行精细控制,我建议使用 System.arraycopy()
。System.arraycopy
使用memmove
操作来移动单词,并在C中使用汇编语言来移动其他原始类型。因此,它会尽其所能以最高效的方式移动尽可能多的内容。
字节码本身就是原生执行的,因此性能很可能比循环更好。
如果使用循环,则必须执行字节码,这将产生开销。而数组复制应该是直接的内存复制。
本地函数应该比JVM函数更快,因为没有虚拟机开销。然而,对于许多(>1000)非常小的(len<10)数组,它可能会更慢。