对于小型数组,Java System.arraycopy()
是否有效,或者它是一个本机方法的事实是否可能比简单的循环和函数调用效率低得多?
本机方法是否会因跨越某种Java系统桥而产生额外的性能开销?
答案 0 :(得分:27)
扩展了Sid所写的内容,System.arraycopy
很可能只是一个JIT内在函数;这意味着当代码调用System.arraycopy
时,它很可能会调用JIT特定的实现(一旦JIT标记System.arraycopy
为“热”),而不是通过JNI接口执行,所以它不会不会产生本机方法的正常开销。
通常,执行本机方法确实会产生一些开销(通过JNI接口,在执行本机方法时也不会发生一些内部JVM操作)。但这不是因为一个方法被标记为“本机”,你实际上是在使用JNI执行它。 JIT可以做一些疯狂的事情。
最简单的检查方法是,正如已经建议的那样,编写一个小基准测试,小心Java微基准测试的常规警告(首先预热代码,避免代码没有副作用,因为JIT只是将其优化为无操作等。)
答案 1 :(得分:23)
这是我的基准代码:
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
调用期间实现预热,因此使用{{1} } = 5就够了;在testRep
= 1000000(1e6)时,预热需要至少2或3次迭代,因此copyCount
应增加以获得可用的结果。
通过我的配置(CPU Intel Core 2 Duo E8500 @ 3.16GHz,Java SE 1.6.0_35-b10和Eclipse 3.7.2),从基准测试中可以看出:
testRep
= 24时,copySize
和手动循环几乎相同的时间(有时一个比另一个快一点,其他时候则相反),System.arraycopy()
< 24,手动循环快于copySize
(System.arraycopy()
= 23时速度稍快,copySize
< 5)真的更快,copySize
> 24,copySize
比手动循环更快(System.arraycopy()
= 25时速度稍快,循环时间/阵列复制时间随着copySize
的增加而增加)。注意:我不是英语母语,请原谅我的所有语法/词汇错误。
答案 2 :(得分:17)
这是一个有效的问题。例如,在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
中,始终使用System.arraycopy()
,永远不会“逐个元素复制”,无论长度如何(即使它为0)。由于ArrayList
非常注重性能,我们可以推导System.arraycopy()
是最有效的数组复制方式,无论长度如何。
答案 3 :(得分:7)
System.arraycopy
使用memmove
操作移动单词和程序集,以便在场景后面的C中移动其他基本类型。所以它尽最大努力移动尽可能多的效率。
答案 4 :(得分:5)
字节代码无论如何都是本地执行的,因此性能可能比循环更好。
因此,在循环的情况下,它必须执行将导致开销的字节代码。阵列副本应该是直接记忆。
答案 5 :(得分:2)
我没有依赖猜测和可能过时的信息,而是使用caliper运行了一些基准测试。事实上,Caliper附带了一些例子,其中包括一个CopyArrayBenchmark
来测量这个问题!你所要做的就是运行
mvn exec:java -Dexec.mainClass=com.google.caliper.runner.CaliperMain -Dexec.args=examples.CopyArrayBenchmark
我的结果基于Oracle的HotSpot(TM)64位服务器VM,1.8.0_31-b13,在2010年中期的MacBook Pro上运行(带有Intel Arrandale i7的macOS 10.11.6,8 GiB RAM)。我不相信发布原始时序数据很有用。相反,我将用支持的可视化来总结结论。
总结:
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
s的情况。)由于这一点以及通话的详细程度,如果您需要精确控制哪些元素被复制到哪里,我建议System.arraycopy()
。以下是时序图:
答案 6 :(得分:-1)
本机函数应该比JVM函数更快,因为没有VM开销。然而,对于许多(> 1000)非常小(len< 10)阵列,它可能更慢。