System.arrayCopy很慢

我一直在尝试测量System.arrayCopy与Arrays.copyOf的性能,以便正确选择其中一个。 仅仅为了基准测试我也添加了手动副本,结果让我感到惊讶。 显然我错过了一些非常重要的东西,请你,告诉我,它是什么? 实现如下(参见前4种方法)。

public class ArrayCopy { public static int[] createArray( int size ) { int[] array = new int[size]; Random r = new Random(); for ( int i = 0; i < size; i++ ) { array[i] = r.nextInt(); } return array; } public static int[] copyByArraysCopyOf( int[] array, int size ) { return Arrays.copyOf( array, array.length + size ); } public static int[] copyByEnlarge( int[] array, int size ) { return enlarge( array, size ); } public static int[] copyManually( int[] array, int size ) { int[] newArray = new int[array.length + size]; for ( int i = 0; i < array.length; i++ ) { newArray[i] = array[i]; } return newArray; } private static void copyArray( int[] source, int[] target ) { System.arraycopy( source, 0, target, 0, Math.min( source.length, target.length ) ); } private static int[] enlarge( int[] orig, int size ) { int[] newArray = new int[orig.length + size]; copyArray( orig, newArray ); return newArray; } public static void main( String... args ) { int[] array = createArray( 1000000 ); int runs = 1000; int size = 1000000; System.out.println( "****************** warm up #1 ******************" ); warmup( ArrayCopy::copyByArraysCopyOf, array, size, runs ); warmup( ArrayCopy::copyByEnlarge, array, size, runs ); warmup( ArrayCopy::copyManually, array, size, runs ); System.out.println( "****************** warm up #2 ******************" ); warmup( ArrayCopy::copyByArraysCopyOf, array, size, runs ); warmup( ArrayCopy::copyByEnlarge, array, size, runs ); warmup( ArrayCopy::copyManually, array, size, runs ); System.out.println( "********************* test *********************" ); System.out.print( "copyByArrayCopyOf" ); runTest( ArrayCopy::copyByArraysCopyOf, array, size, runs ); System.out.print( "copyByEnlarge" ); runTest( ArrayCopy::copyByEnlarge, array, size, runs ); System.out.print( "copyManually" ); runTest( ArrayCopy::copyManually, array, size, runs ); } private static void warmup( BiConsumer consumer, int[] array, int size, int runs ) { for ( int i = 0; i < runs; i++ ) { consumer.accept( array, size ); } } private static void runTest( BiConsumer consumer, int[] array, int size, int runs ) { ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long currentCpuTime = threadMXBean.getCurrentThreadCpuTime(); long nanoTime = System.nanoTime(); for ( int i = 0; i < runs; i++ ) { consumer.accept( array, size ); } System.out.println( "-time = " + ( ( System.nanoTime() - nanoTime ) / 10E6 ) + " ms. CPU time = " + ( ( threadMXBean.getCurrentThreadCpuTime() - currentCpuTime ) / 10E6 ) + " ms" ); } } 

结果表明,手动复制的性能提高了约30%,如下所示:

 ****************** warm up #1 ****************** ****************** warm up #2 ****************** ********************* test ********************* copyByArrayCopyOf-time = 162.470107 ms. CPU time = 153.125 ms copyByEnlarge-time = 168.6757949 ms. CPU time = 164.0625 ms copyManually-time = 116.3975962 ms. CPU time = 110.9375 ms 

我真的很困惑,因为我认为(并且我可能仍然这样做) System.arrayCopy由于它的诞生是复制数组的最佳方式,但我无法解释这个结果。

实际上,HotSpot编译器非常智能,可以展开和矢量化手动复制循环 – 这就是为什么结果代码似乎得到了很好的优化。

为什么System.arraycopy慢? 它最初是一个本机方法,您必须支付本机调用,直到编译器将其优化为JVM内部。

但是,在您的测试中,编译器没有机会进行这样的优化,因为enlarge方法的调用次数不够多(即它不被认为是热的)。

我会告诉你一个有趣的技巧来强制优化。 重写enlarge方法如下:

 private static int[] enlarge(int[] array, int size) { for (int i = 0; i < 10000; i++) { /* fool the JIT */ } int[] newArray = new int[array.length + size]; System.arraycopy(array, 0, newArray, 0, array.length); return newArray; } 

空循环触发后备计数器溢出,这反过来触发enlarge方法的编译。 然后从编译的代码中消除空循环,因此它是无害的。 现在enlarge方法比手动循环1.5倍

System.arraycopy紧跟new int[]非常重要。 在这种情况下,HotSpot可以优化掉新分配的arrays的冗余归零。 您知道,所有Java对象必须在创建后立即归零。 但是,只要编译器检测到数组在创建后立即被填充,它就可以消除归零,从而使结果代码更快。

PS @assylias的基准测试很好,但它也受制于System.arraycopy不适用于大型数组的事实。 在小型arrays的情况下,每秒多次调用arrayCopy基准测试,JIT认为它很热并且优化得很好。 但是对于大型数组,每次迭代都会更长,因此每秒的迭代次数要少得多,并且JIT不会将arrayCopy视为热点。

使用jmh,我得到下表中显示的结果(size是数组的大小,score是以微秒为单位的时间,error表示置信区间为99.9%):

 Benchmark (size) Mode Cnt Score Error Units ArrayCopy.arrayCopy 10 avgt 60 0.022 ± 0.001 us/op ArrayCopy.arrayCopy 10000 avgt 60 4.959 ± 0.068 us/op ArrayCopy.arrayCopy 10000000 avgt 60 11906.870 ± 220.850 us/op ArrayCopy.clone_ 10 avgt 60 0.022 ± 0.001 us/op ArrayCopy.clone_ 10000 avgt 60 4.956 ± 0.068 us/op ArrayCopy.clone_ 10000000 avgt 60 10895.856 ± 208.369 us/op ArrayCopy.copyOf 10 avgt 60 0.022 ± 0.001 us/op ArrayCopy.copyOf 10000 avgt 60 4.958 ± 0.072 us/op ArrayCopy.copyOf 10000000 avgt 60 11837.139 ± 220.452 us/op ArrayCopy.loop 10 avgt 60 0.036 ± 0.001 us/op ArrayCopy.loop 10000 avgt 60 5.872 ± 0.095 us/op ArrayCopy.loop 10000000 avgt 60 11315.482 ± 217.348 us/op 

实际上,对于大型数组,循环似乎比arrayCopy稍微好一点 – 可能是因为JIT非常擅长优化这样一个简单的循环。 对于较小的arrays,arrayCopy似乎更好(尽管差异非常小)。

但请注意,克隆似乎始终与其他选项一样好或更好,具体取决于大小。 所以我会选择克隆,这也更容易使用。


供参考,基准代码,运行-wi 5 -w 1000ms -i 30 -r 1000ms -t 1 -f 2 -tu us

 @State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) public class ArrayCopy { @Param({"10", "10000", "10000000"}) int size; private int[] array; @Setup(Level.Invocation) public void setup() { array = new int[size]; for (int i = 0; i < size; i++) { array[i] = i; } } @Benchmark public int[] clone_() { int[] copy = array.clone(); return copy; } @Benchmark public int[] arrayCopy() { int[] copy = new int[array.length]; System.arraycopy(array, 0, copy, 0, array.length); return copy; } @Benchmark public int[] copyOf() { int[] copy = Arrays.copyOf(array, array.length); return copy; } @Benchmark public int[] loop() { int[] copy = new int[array.length]; for (int i = 0; i < array.length; i++) { copy[i] = array[i]; } return copy; } }