为什么clone()是复制数组的最佳方法?

这对我来说是一种耻辱,但我不知道:

您应该使用clone来复制数组,因为这通常是最快的方法。

正如Josh Bloch在本博客中所述: http : //www.artima.com/intv/bloch13.html

我总是使用System.arraycopy(...) 。 这两种方法都是原生的,所以可能没有深入了解我无法弄清楚的库的来源,为什么会如此。

我的问题很简单:为什么它是最快的方式? System.arraycopy什么区别? 这里解释了不同之处,但它没有回答为什么Josh Bloch认为clone()是最快的方法。

我想说明为什么clone()是复制数组的最快方法,而不是System.arraycopy(..)或其他:

1. clone()在将源数组复制到目标数据库之前不必进行类型检查。 它只是简单地分配新的内存空间并将对象分配给它。 另一方面, System.arraycopy(..)检查类型,然后复制数组。

2. clone()还会中断优化以消除冗余归零。 如您所知,Java中每个已分配的数组都必须使用0s或相应的默认值进行初始化。 但是,如果JIT在创建后立即填充数组,则可以避免将该数组归零。 与使用现有0s或相应的默认值更改复制值相比,这确实更快。 使用System.arraycopy(..)花费大量时间清除和复制已初始化的数组。 为此,我已经完成了一些基准测试。

 @BenchmarkMode(Mode.Throughput) @Fork(1) @State(Scope.Thread) @Warmup(iterations = 10, time = 1, batchSize = 1000) @Measurement(iterations = 10, time = 1, batchSize = 1000) public class BenchmarkTests { @Param({"1000","100","10","5", "1"}) private int size; private int[] original; @Setup public void setup() { original = new int[size]; for (int i = 0; i < size; i++) { original[i] = i; } } @Benchmark public int[] SystemArrayCopy() { final int length = size; int[] destination = new int[length]; System.arraycopy(original, 0, destination, 0, length); return destination; } @Benchmark public int[] arrayClone() { return original.clone(); } } 

输出:

 Benchmark (size) Mode Cnt Score Error Units ArrayCopy.SystemArrayCopy 1 thrpt 10 26324.251 ± 1532.265 ops/s ArrayCopy.SystemArrayCopy 5 thrpt 10 26435.562 ± 2537.114 ops/s ArrayCopy.SystemArrayCopy 10 thrpt 10 27262.200 ± 2145.334 ops/s ArrayCopy.SystemArrayCopy 100 thrpt 10 10524.117 ± 474.325 ops/s ArrayCopy.SystemArrayCopy 1000 thrpt 10 984.213 ± 121.934 ops/s ArrayCopy.arrayClone 1 thrpt 10 55832.672 ± 4521.112 ops/s ArrayCopy.arrayClone 5 thrpt 10 48174.496 ± 2728.928 ops/s ArrayCopy.arrayClone 10 thrpt 10 46267.482 ± 4641.747 ops/s ArrayCopy.arrayClone 100 thrpt 10 19837.480 ± 364.156 ops/s ArrayCopy.arrayClone 1000 thrpt 10 1841.145 ± 110.322 ops/s 

根据输出我得到的clone几乎快两倍来自System.arraycopy(..)

3.此外,使用像clone()这样的手动复制方法可以获得更快的输出,因为它不需要进行任何VM调用(与System.arraycopy()不同)。

首先, clone()不必执行System.arraycopy()所做的类型检查。

我想纠正和补充以前的答案。

  1. Object.clone对数组使用未经检查的System.arraycopy实现;
  2. Object.clone的主要性能改进,它是直接初始化RAW内存。 在System.arraycopy的情况下,它也尝试将数组初始化与复制操作结合起来,正如我们在源代码中看到的那样,但它也会对此进行不同的额外检查,这与Object.clone不同。 如果您只是禁用此function(见下文),那么性能将非常接近(特别是在我的硬件上)。
  3. 另一个有趣的事情是关于Young vs Old Gen。如果源arrays对齐并且在Old Gen内部,两种方法都具有接近的性能。
  4. 当我们复制原始数组时,System.arraycopy总是使用generate_unchecked_arraycopy。
  5. 它取决于硬件/操作系统相关的实现,因此不要相信基准测试和假设,请自行检查。

说明

首先,克隆方法和System.arraycopy是内在函数。 Object.clone和System.arraycopy使用generate_unchecked_arraycopy。 如果我们更深入,我们可以看到在HotSpot之后选择具体实现,依赖于操作系统等。

朗力。 我们来看看Hotspot的代码。 首先,我们将看到Object.clone(LibraryCallKit :: inline_native_clone)使用generate_arraycopy,在-XX:-ReduceInitialCardMarks的情况下,它用于System.arraycopy。 否则它会执行LibraryCallKit :: copy_to_clone,它会在RAW内存中初始化新数组(如果-XX:+ ReduceBulkZeroing,默认情况下启用)。 相比之下,System.arraycopy直接使用generate_arraycopy,尝试检查ReduceBulkZeroing(以及许多其他情况)并消除数组归零,并提及额外的检查,并且还会进行额外的检查以确保所有元素都已初始化,这与Object.clone不同。 最后,在最好的情况下,它们都使用generate_unchecked_arraycopy。

下面我展示一些基准测试,看看这对实践的影响:

  1. 第一个是简单的基准测试,与之前的答案的唯一区别是,数组没有排序; 我们看到arraycopy较慢(但不是两次),结果 – https://pastebin.com/ny56Ag1z ;
  2. 其次,我添加选项-XX:-ReduceBulkZeroing,现在我看到两种方法的性能非常接近。 结果 – https://pastebin.com/ZDAeQWwx ;
  3. 我还假设我们将在Old / Young之间有区别,因为数组对齐(它是Java GC的一个特性,当我们调用GC时,数组的对齐被更改,使用JOL很容易观察)。 令我感到惊讶的是,这两种方法的性能变得一致,并且降级。 结果 – https://pastebin.com/bTt5SJ8r 。 对于相信具体数字的人来说,System.arraycopy的吞吐量比Object.clone更好。

第一个基准:

 import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) @BenchmarkMode(Mode.All) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class CloneVsArraycopy { @Param({"10", "1000", "100000"}) int size; int[] source; @Setup(Level.Invocation) public void setup() { source = create(size); } @Benchmark public int[] clone(CloneVsArraycopy cloneVsArraycopy) { return cloneVsArraycopy.source.clone(); } @Benchmark public int[] arraycopy(CloneVsArraycopy cloneVsArraycopy) { int[] dest = new int[cloneVsArraycopy.size]; System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length); return dest; } public static void main(String[] args) throws Exception { new Runner(new OptionsBuilder() .include(CloneVsArraycopy.class.getSimpleName()) .warmupIterations(20) .measurementIterations(20) .forks(20) .build()).run(); } private static int[] create(int size) { int[] a = new int[size]; for (int i = 0; i < a.length; i++) { a[i] = ThreadLocalRandom.current().nextInt(); } return a; } } 

在我的电脑上运行此测试,我得到了这个 - https://pastebin.com/ny56Ag1z 。 差异不是很大,但仍然存在。

第二个基准测试我只添加了一个设置-XX:-ReduceBulkZeroing并得到了这个结果https://pastebin.com/ZDAeQWwx 。 不,我们认为对于Young Gen来说差异也小得多。

在第三个基准测试中,我只更改了设置方法并启用了ReduceBulkZeroing选项:

 @Setup(Level.Invocation) public void setup() { source = create(size); // try to move to old gen/align array for (int i = 0; i < 10; ++i) { System.gc(); } } 

差异要小得多(可能是错误间隔) - https://pastebin.com/bTt5SJ8r

放弃

这也可能是错的。 你应该自己检查一下。

此外

我认为,看看基准测试过程很有意思:

 # Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.arraycopy # Parameters: (size = 50000) # Run progress: 0,00% complete, ETA 00:07:30 # Fork: 1 of 5 # Warmup Iteration 1: 8,870 ops/ms # Warmup Iteration 2: 10,912 ops/ms # Warmup Iteration 3: 16,417 ops/ms <- Hooray! # Warmup Iteration 4: 17,924 ops/ms <- Hooray! # Warmup Iteration 5: 17,321 ops/ms <- Hooray! # Warmup Iteration 6: 16,628 ops/ms <- What! # Warmup Iteration 7: 14,286 ops/ms <- No, stop, why! # Warmup Iteration 8: 13,928 ops/ms <- Are you kidding me? # Warmup Iteration 9: 13,337 ops/ms <- pff # Warmup Iteration 10: 13,499 ops/ms Iteration 1: 13,873 ops/ms Iteration 2: 16,177 ops/ms Iteration 3: 14,265 ops/ms Iteration 4: 13,338 ops/ms Iteration 5: 15,496 ops/ms 

对于Object.clone

 # Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.clone # Parameters: (size = 50000) # Run progress: 0,00% complete, ETA 00:03:45 # Fork: 1 of 5 # Warmup Iteration 1: 8,761 ops/ms # Warmup Iteration 2: 12,673 ops/ms # Warmup Iteration 3: 20,008 ops/ms # Warmup Iteration 4: 20,340 ops/ms # Warmup Iteration 5: 20,112 ops/ms # Warmup Iteration 6: 20,061 ops/ms # Warmup Iteration 7: 19,492 ops/ms # Warmup Iteration 8: 18,862 ops/ms # Warmup Iteration 9: 19,562 ops/ms # Warmup Iteration 10: 18,786 ops/ms 

我们可以在System.arraycopy中观察性能降级。 我看到Streams的相似图片,编译器中有一个错误。 我想它也可能是编译器中的一个bug。 无论如何,经过3次热身表现降级后很奇怪。

UPDATE

什么是关于类型检查

 import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @State(Scope.Benchmark) @BenchmarkMode(Mode.All) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class CloneVsArraycopyObject { @Param({"100"}) int size; AtomicLong[] source; @Setup(Level.Invocation) public void setup() { source = create(size); } @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public AtomicLong[] clone(CloneVsArraycopyObject cloneVsArraycopy) { return cloneVsArraycopy.source.clone(); } @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public AtomicLong[] arraycopy(CloneVsArraycopyObject cloneVsArraycopy) { AtomicLong[] dest = new AtomicLong[cloneVsArraycopy.size]; System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length); return dest; } public static void main(String[] args) throws Exception { new Runner(new OptionsBuilder() .include(CloneVsArraycopyObject.class.getSimpleName()) .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining", "-XX:-ReduceBulkZeroing") .warmupIterations(10) .measurementIterations(5) .forks(5) .build()) .run(); } private static AtomicLong[] create(int size) { AtomicLong[] a = new AtomicLong[size]; for (int i = 0; i < a.length; i++) { a[i] = new AtomicLong(ThreadLocalRandom.current().nextLong()); } return a; } } 

没有观察到差异 - https://pastebin.com/ufxCZVaC 。 我想一个解释很简单,因为在这种情况下System.arraycopy是热内在的,真正的实现只是内联而没有任何类型转换等。

注意

我同意Radiodef你会发现有趣的阅读博客文章 ,这个博客的作者是JMH的创建者(或创作者之一)。

就拷贝而言, System.arrayCopy是当时和现在最快的。

  • System.arrayCopy不会创建新数组,也不能以原始复制速度进行打败。
  • Arrays.copyOf只是创建一个数组并调用arrayCopy 。 方便。
  • Array.clone非常高效,但它需要将复制的数据刷新到所有cpu缓存。

如果您可以通过使用arrayCopy重用数组的方式进行arrayCopy ,那就去吧。 否则我个人推荐copyOf给出cpu核心的上升趋势,并且因为克隆被认为是旧的并且总体上存在问题 – Josh Bloch博客的主要观点是开始这个​​问题。

与常见的想法相反,实际的复制循环(类型是否已检查) 不是 Java字节码,并且热点无法优化。 循环用C ++编码,是低级jvm实现。


答案很长:

这个答案基于并链接到OpenJDK 8的源代码,据我所知,对于Sun来说应该是相同的。

数组副本可能比大多数人想象的要复杂得多。 在C代码级别,它可以分为三种情况:

  1. 使用复制循环 直接复制原始数组。
  2. 同样类的对象数组或超类数组的子类也可以直接复制 。
  3. 否则,在不同类的数组之间,对每个元素进行类型检查。

复制数组的绝对速度因此在很大程度上取决于数组类型。 但是,这三种克隆方法的相对速度并不是因为它们都解析为相同的复制循环,内联的C ++或汇编循环。 因此,速度的不同主要是由间接费用和其他因素引起的。

  • System.arrayCopy本质上是类型检查和长度检查,然后直接到复制循环。 在我自己的测试中, arrayCopy总是比其他两种方法更快,超出任何误差范围。

  • Arrays.copyOf只是创建一个新数组调用System.arrayCopy 。 请注意,它不会调用Array.clone。 与Radiodef的评论相反,没有迹象表明Java 8将绕过零初始化。

  • Array.clone很有意思。 它使用最少的检查直接调用堆分配和复制循环。 因此,它的数组创建应该比Arrays.copyOf更快,并且它的副本与System.arrayCopy一样快,如果不是更快的话。

但在我的测试中, Array.clonecopyOf略慢。

我怀疑这是因为复制后的内存障碍 。 像构造函数一样, clone将确保复制的数据对所有线程都可见 – System.arrayCopyArray.copyOf都不会这样做。 这意味着Array.clone需要花时间等待CPU缓存更新。

如果这是真的,则Array.cloneArrays.copyOf的结果取决于clone的缓存刷新是否比Arrays.copyOf开销更快,并且应该是平台相关的。

除此之外,由于克隆总是产生相同类型的数组,因此所有三种方法最终都使用相同的复制循环。

如果您只想复制,则arrayCopy总是最快的,因为它不会创建新数组。 对于其他人来说,如果java邮件列表是可以接受的,那么Arrays.copyOfArray.clone之间的选择应该主要是品味问题 。


我的jmh测试结果和代码如下。

  • 一种方法测试返回复制的数组。
  • 双向测试覆盖复制源,强制下一个克隆复制“新”数据。
  • NoClone不会克隆任何东西,并且是确保更高速度更快的标准。

如上所述,克隆和CopyOf是一场紧密的比赛,你的里程可能会有所不同。

 /* # Run complete. Total time: 00:06:44 Benchmark Mode Cnt Score Error Units MyBenchmark.ArrayCloneByteOneWay thrpt 20 1048588.503 ± 2608.862 ops/s MyBenchmark.ArrayCloneByteTwoWay thrpt 20 523782.848 ± 1613.823 ops/s MyBenchmark.ArrayCloneObjOneWay thrpt 20 260903.006 ± 1311.827 ops/s MyBenchmark.ArrayCloneObjTwoWay thrpt 20 129448.639 ± 1179.122 ops/s MyBenchmark.ArraysCopyOfByteOneWay thrpt 20 1065995.804 ± 2197.919 ops/s MyBenchmark.ArraysCopyOfByteTwoWay thrpt 20 533025.610 ± 2831.955 ops/s MyBenchmark.ArraysCopyOfObjOneWay thrpt 20 266134.565 ± 1536.756 ops/s MyBenchmark.ArraysCopyOfObjTwoWay thrpt 20 130821.380 ± 274.325 ops/s MyBenchmark.NoClone thrpt 20 308776528.157 ± 2546848.128 ops/s MyBenchmark.SystemArrayCopyByteOneWay thrpt 20 1232733.367 ± 8439.409 ops/s MyBenchmark.SystemArrayCopyByteTwoWay thrpt 20 859387.983 ± 1919.359 ops/s MyBenchmark.SystemArrayCopyObjOneWay thrpt 20 239532.442 ± 775.193 ops/s MyBenchmark.SystemArrayCopyObjTwoWay thrpt 20 167235.661 ± 503.141 ops/s */ import java.util.Arrays; import java.util.Random; import org.openjdk.jmh.annotations.*; @Fork(2) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 10, time = 1) public class Q46230557 { private static final int ARRAY_SIZE = 8192; @State(Scope.Thread) public static class Data { public byte[] bytes = new byte[ ARRAY_SIZE ]; public Object[] objs = new Object[ ARRAY_SIZE ]; @Setup public void setup() { final Random RNG = new Random(); RNG.nextBytes( bytes ); for ( int i = 0 ; i < ARRAY_SIZE ; i++ ) objs[i] = RNG.nextInt(); } } @Benchmark public byte[] NoClone( final Data data ) { return data.bytes; } @Benchmark public byte[] SystemArrayCopyByteOneWay( final Data data ) { final byte[] dest = new byte[ ARRAY_SIZE ]; System.arraycopy( data.bytes, 0, dest, 0, ARRAY_SIZE ); return dest; } @Benchmark public byte[] SystemArrayCopyByteTwoWay( final Data data ) { final byte[] buf = new byte[ ARRAY_SIZE ]; System.arraycopy( data.bytes, 0, buf, 0, ARRAY_SIZE ); System.arraycopy( buf, 0, data.bytes, 0, ARRAY_SIZE ); return data.bytes; } @Benchmark public byte[] ArraysCopyOfByteOneWay( final Data data ) { return Arrays.copyOf( data.bytes, ARRAY_SIZE ); } @Benchmark public byte[] ArraysCopyOfByteTwoWay( final Data data ) { final byte[] buf = Arrays.copyOf( data.bytes, ARRAY_SIZE ); return data.bytes = Arrays.copyOf( buf, ARRAY_SIZE ); } @Benchmark public byte[] ArrayCloneByteOneWay( final Data data ) { return data.bytes.clone(); } @Benchmark public byte[] ArrayCloneByteTwoWay( final Data data ) { final byte[] buf = data.bytes.clone(); return data.bytes = buf.clone(); } @Benchmark public Object[] SystemArrayCopyObjOneWay( final Data data ) { final Object[] dest = new Object[ ARRAY_SIZE ]; System.arraycopy( data.objs, 0, dest, 0, ARRAY_SIZE ); return dest; } @Benchmark public Object[] SystemArrayCopyObjTwoWay( final Data data ) { final Object[] buf = new Object[ ARRAY_SIZE ]; System.arraycopy( data.objs, 0, buf, 0, ARRAY_SIZE ); System.arraycopy( buf, 0, data.objs, 0, ARRAY_SIZE ); return data.objs; } @Benchmark public Object[] ArraysCopyOfObjOneWay( final Data data ) { return Arrays.copyOf( data.objs, ARRAY_SIZE ); } @Benchmark public Object[] ArraysCopyOfObjTwoWay( final Data data ) { final Object[] buf = Arrays.copyOf( data.objs, ARRAY_SIZE ); return data.objs = Arrays.copyOf( buf, ARRAY_SIZE ); } @Benchmark public Object[] ArrayCloneObjOneWay( final Data data ) { return data.objs.clone(); } @Benchmark public Object[] ArrayCloneObjTwoWay( final Data data ) { final Object[] buf = data.objs.clone(); return data.objs = buf.clone(); } } 

性能的差异来自跳过数组被清零的步骤。

 public static int[] copyUsingArraycopy(int[] original) { // Memory is allocated and zeroed out int[] copy = new int[original.Length]; // Memory is copied System.arraycopy(original, 0, copy, 0, original.length); } public static int[] copyUsingClone(int[] original) { // Memory is allocated, but not zeroed out // Unitialized memory is then copied into return (int[])original.clone(); } 

然而,在复制arrays的性能产生显着差异的情况下,通常更好地采用双缓冲。

 int[] backBuffer = new int[BUFFER_SIZE]; int[] frontBuffer = new int[BUFFER_SIZE]; ... // Swap buffers int[] temp = frontBuffer; frontBuffer = backBuffer; backBuffer = temp; System.arraycopy(frontBuffer, 0, backBuffer, 0, BUFFER_SIZE);