为什么Java中的System.arraycopy方法是本地方法?

93

我看到Java源代码中使用了System.arraycopy本地方法,感到很惊讶。

当然,原因是这样更快。但是,这段代码能够利用哪些本地技巧使它更快呢?

为什么不只是循环遍历原始数组并将每个指针复制到新数组中-这显然不会那么慢和繁琐吧?

5个回答

87
在本地代码中,可以使用单个 memcpy / memmove 来完成,而不是进行 n 次独立的复制操作。性能差异很大。

9
实际上,只有 arraycopy 的某些子情况可以使用 memcpy / memmove 实现。其他情况需要对每个复制的元素进行运行时类型检查。 - Stephen C
1
@Stephen C,有趣 - 为什么呢? - Péter Török
3
考虑将一个由String对象填充的Object[]复制到一个String[]中。请参见http://java.sun.com/javase/6/docs/api/java/lang/System.html#arraycopy(java.lang.Object,%20int,%20java.lang.Object,%20int,%20int)的最后一段。 - Stephen C
3
Peter,Object[],byte[]和char[]是最常复制的对象,它们都不需要显式类型检查。编译器聪明到只有在必要的情况下才会检查,并且实际上在99.9%的情况下不需要检查。有趣的是,小尺寸的拷贝(小于缓存行)相当占主导地位,因此对于小尺寸的东西来说,“memcpy”快速复制确实非常重要。 - bestsss
1
@jainilvachhani,memcpymemmove都是O(n)的,但由于例如simd优化,它们会快几倍,因此您可以说它们是O(n/x),其中x取决于这些函数中使用的优化。 - ufoq
显示剩余3条评论

16

这段代码无法用Java编写。原生代码可以忽略或省略对象数组和基本类型数组之间的区别,而Java不能,至少效率不高。

另外,由于重叠数组所需的语义,它也无法使用单个memcpy()函数编写。


5
好的,那就用memmove吧。虽然我认为在这个问题的上下文中并没有太大的区别。 - Péter Török
1
如果源数组和目标数组相同且仅偏移量不同,则称为重叠数组。虽然memcpy()的行为已经被仔细规定,但它并不遵守重叠数组情况。 - user207421
1
它不能用Java编写吗?难道不能编写一个通用方法来处理Object的子类,然后为每个原始类型编写一个方法吗? - Michael Dorst
@anthropomorphic 请仔细阅读我所写的内容。“至少不是高效的。”这样的实现需要对数据类型做出决策,然后分支到您描述的代码之一。 - user207421
@MichaelDorst 泛型是在JDK 1.5中引入的,而System.arrayCopy()方法则是在1.0或1.1中引入的。 - AwesomeHunter
显示剩余3条评论

12

当然,这取决于具体实现。

HotSpot将其视为“内置函数”,并在调用点插入代码。 这是机器码,而不是缓慢的旧C代码。 这也意味着该方法签名的问题基本上会消失。

一个简单的复制循环足够简单,可以对其应用明显的优化措施,例如循环展开。 实际发生的事情再次取决于具体实现。


3
这是一个非常好的答案 :),尤其是提到了内在函数。如果没有它们,简单的迭代可能会更快,因为通常会被 JIT 展开。 - bestsss

4

在我的测试中,对于复制多维数组,System.arraycopy() 比交错使用循环快10到20倍:

float[][] foo = mLoadMillionsOfPoints(); // result is a float[1200000][9]
float[][] fooCpy = new float[foo.length][foo[0].length];
long lTime = System.currentTimeMillis();
System.arraycopy(foo, 0, fooCpy, 0, foo.length);
System.out.println("native duration: " + (System.currentTimeMillis() - lTime) + " ms");
lTime = System.currentTimeMillis();

for (int i = 0; i < foo.length; i++)
{
    for (int j = 0; j < foo[0].length; j++)
    {
        fooCpy[i][j] = foo[i][j];
    }
}
System.out.println("System.arraycopy() duration: " + (System.currentTimeMillis() - lTime) + " ms");
for (int i = 0; i < foo.length; i++)
{
    for (int j = 0; j < foo[0].length; j++)
    {
        if (fooCpy[i][j] != foo[i][j])
        {
            System.err.println("ERROR at " + i + ", " + j);
        }
    }
}

这将打印:

System.arraycopy() duration: 1 ms
loop duration: 16 ms

11
虽然这个问题早已过时,但只是为了记录:这并不是一个公平的基准测试(更不用说是否首先进行此类基准测试的问题)。System.arraycopy执行的是浅拷贝(仅复制对内部float[]引用),而您嵌套的for循环执行的是深拷贝(按float逐一复制)。使用System.arraycopyfooCpy[i][j]的更改将反映在foo中,但不会使用嵌套的for循环。 - misberner

4
有几个原因:
  1. JIT编译器不太可能生成和手写的C代码一样高效的底层代码。使用低级别的C可以实现许多优化,这些优化对于通用JIT编译器来说几乎是不可能的。

    参见此链接以获取一些手写C实现(memcpy,但原理相同)的技巧和速度比较:查看此Optimizing Memcpy improves speed

  2. C版本基本上独立于数组成员的类型和大小。在Java中无法做到这一点,因为无法将数组内容作为原始内存块(例如指针)获取。


1
Java代码可以进行优化。实际上,生成的机器码比C更高效。 - Tom Hawtin - tackline
1
我认为有时候 JIT 编译出的代码会更好地进行本地优化,因为它知道运行在哪个处理器上。然而,由于 JIT 是“即时编译”,它永远无法使用那些需要更长时间执行的非局部优化。此外,它也永远无法与手工编写的 C 代码相匹配(可能还要考虑到处理器,并通过编译成针对特定处理器的代码或某种运行时检查来部分抵消 JIT 的优势)。 - Hrvoje Prgeša
1
我认为Sun JIT编译器团队会对这些观点提出异议。例如,我相信HotSpot可以进行全局优化以消除不必要的方法调度,并且没有理由一个JIT不能生成特定于处理器的代码。还有一个观点是JIT编译器可以基于当前应用程序运行的执行行为进行分支优化。 - Stephen C
1
System.arrayCopy并没有使用C语言实现,这有点使得这个答案失去了效力。 - Nitsan Wakart
@Nitsan Wakart:理论上,JIT 可以始终将方法调用重定向到一个特别准备的方法,绕过默认的 Java 方法。不确定是否曾经使用过... - Hrvoje Prgeša
显示剩余2条评论

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