Java的System.arraycopy()对于小数组是否高效?

56

Java的System.arraycopy()方法对于小数组来说是否高效,还是因为它是本地方法而可能比简单循环和函数调用方式效率明显低?

本地方法是否会因为跨越某种Java系统桥梁而产生额外的性能开销?


10
你试过并进行过基准测试吗? - corsiKa
2
我很想看到这方面的微基准测试。 - Thomas Jungblut
1
我认为内置的本地代码不受JNI延迟的影响。 - gd1
@glowcoder,我想看看你的微基准测试,那是一个非常难以正确实现的。 - bestsss
回答:有一个特定的基准测试会执行大量小型(小于缓存行)的复制操作,所有 JDK 实现者都知道它,并希望针对这种情况进行优化。 - bestsss
显示剩余2条评论
7个回答

28
稍微解释一下Sid所写的内容,很有可能System.arraycopy只是一个JIT内置函数;这意味着当代码调用System.arraycopy时,它很可能会调用一个JIT特定的实现(一旦JIT将System.arraycopy标记为“热点”),该实现不通过JNI接口执行,因此不会产生本地方法的常规开销。
通常情况下,执行本地方法确实会有一些开销(需要通过JNI接口,也有一些内部JVM操作无法在执行本地方法时进行)。但仅仅是因为一个方法被标记为“native”,并不意味着你实际上正在使用JNI来执行它。JIT可以做出一些奇怪的事情。
最简单的检查方法,就像之前建议的那样,编写一个小型基准测试,并注意Java微基准测试的常规警告(首先预热代码,避免没有副作用的代码,因为JIT会将其优化为空操作等) 。

@vanze,完全在L1缓存中运行的小型基准测试是错误的。 - bestsss
2
@bestsss:他可能是指代码量小,而不一定是我要复制的数组数量少。 - Gravity
@Gravity,大多数人几乎总是错误地进行微基准测试,尤其是Java的。基本上,您需要知道JIT发出什么样的代码才能编写适当的微基准测试。通常最简单的方法是查看生成的汇编代码并将结果与预期进行比较。 - bestsss

24

这是我的基准测试代码:

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以获得可用的结果。
根据我的配置(CPU Intel Core 2 Duo E8500 @ 3.16GHz,Java SE 1.6.0_35-b10和Eclipse 3.7.2),从基准测试中看出以下情况:
  • copySize=24时,System.arraycopy()和手动循环所需时间几乎相同(有时其中一个略微快于另一个,有时则相反),
  • copySize<24时,手动循环比System.arraycopy()更快(copySize=23时略微快,copySize<5时则更快),
  • copySize>24时,System.arraycopy()比手动循环更快(copySize=25时略微快,随着copySize的增加,循环时间/数组拷贝时间的比例也会增加)。
注意:我不是英语母语使用者,请原谅我的语法/词汇错误。

1
我得到了不同的结果。即使是小型数组,似乎也没有关系。对于成千上万的大型数组,systemarraycopy要快得多。对于小型数组,我看不出有什么区别。 - RickHigh
1
我也得到了不同的结果。当数组大小为5000时,System.arrayCopy大约快了4倍,而当数组大小仅为5时,它仍然快了约20%。在大小为2时似乎趋于平衡。因此,我认为没有理由不使用System.arrayCopy。 - nikdeapen
1
@nikdeapen 看起来这种情况在这些年里发生了变化。 - Alex Salauyou

19

这是一个合理的担忧。例如,在 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()是无论长度如何都最高效的数组复制方式。


11
我想问题的一部分是System.arraycopy()是否完全通过JNI。正如有人指出的那样,仅仅因为它被声明为“native”并不意味着任何事情,因为JVM允许具有各种特殊优化。 - Gravity
我对ArrayList类也有同样的想法,即使在我提出这个问题时也是如此:ArrayList被认为是一个非常优化的类,不会执行任何不必要的昂贵操作。但是,也许作者们认为小型ArrayLists的性能并不那么重要(因为最昂贵的操作通常是在大型数据集上进行的),或者它在所有JVM中的效率都不一致。我需要进行基准测试才能信服。 - Gravity
2
他们将关注许多小数组列表的性能。因此,每个小数组列表都必须表现良好。 - irreputable
3
@Gravity,System.arraycopy不是JNI,它是内部函数。 - bestsss
DirectBuffer的复制是一个特殊情况,它根本不使用System.arraycopy或JNI,文档只是旧的。它是内部函数,Unsafe.copymemory基本上是memmove/memcpy,但如果消除了范围检查,简单复制也可以同样快。 - bestsss
显示剩余2条评论

8

我没有依赖猜测和可能过时的信息,而是使用运行了一些基准测试。实际上,Caliper带有一些示例,包括一个CopyArrayBenchmark,可以精确地测量这个问题!你只需要运行它。

mvn exec:java -Dexec.mainClass=com.google.caliper.runner.CaliperMain -Dexec.args=examples.CopyArrayBenchmark

我的结果基于Oracle的Java HotSpot(TM) 64位服务器VM,1.8.0_31-b13,在一台中2010年的MacBook Pro上运行(macOS 10.11.6,Intel Arrandale i7,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 的情况。)因此,由于调用的冗长和需要对要复制的元素进行精细控制,我建议使用 System.arraycopy()
这里是时间图表:

Timings for copying arrays of length 5 Timings for copying arrays of length 500 Timings for copying arrays of length 50000


7

System.arraycopy使用memmove操作来移动单词,并在C中使用汇编语言来移动其他原始类型。因此,它会尽其所能以最高效的方式移动尽可能多的内容。


5

字节码本身就是原生执行的,因此性能很可能比循环更好。

如果使用循环,则必须执行字节码,这将产生开销。而数组复制应该是直接的内存复制。


2
从Java到本地代码的转换会产生额外的成本。 - Andy Thomas
@AndyThomas-Cramer,就上下文切换而言,并不存在 JNI 的本机代码。并且顺便提一句:JIT 足够智能,可以通过循环优化数组复制的同一段代码,就像“System.arraycopy”一样。 - bestsss
“JIT足够智能,可以通过循环来优化数组复制,使其与System.arraycopy相同的代码一样。” -- 是吗?理论上这听起来很有道理,但我没有听说过这样的事情。您可以提供一些参考资料吗? - Gravity
所有的字节码最终都会被视为机器码运行。它可以由JIT解释或编译。因此,根据这个基础,System.Array.Copy可能很可能被转换为memcpy。而循环则可能更昂贵。 - Sid Malani
@SidMalani:将代码编译为JNI调用和编译为memcpy(实际上是memmove)调用之间存在巨大差异,尽管第二个选项可以获得更快的代码,但人类需要为每个内部函数添加JVM的特殊情况。 - Blaisorblade

-1

本地函数应该比JVM函数更快,因为没有虚拟机开销。然而,对于许多(>1000)非常小的(len<10)数组,它可能会更慢。


你能详细解释一下吗?为什么对于许多小数组来说速度会更慢呢? - Costi Ciudatu
我猜是因为函数调用开销吗?除非即使是JIT内部的本地函数也可以被内联... - Gravity
是的,本地函数的调用开销可能会更大。随着累加,它可能会超过通过消除JVM开销节省的时间。 - laci37

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接