System.arrayCopy很慢

时间:2016-10-27 13:56:44

标签: java performance 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<int[], Integer> consumer, int[] array, int size, int runs ) {
        for ( int i = 0; i < runs; i++ ) {
            consumer.accept( array, size );
        }
    }

    private static void runTest( BiConsumer<int[], Integer> 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由于它的诞生是复制数组的最佳方式,但我无法解释这个结果。

2 个答案:

答案 0 :(得分:26)

实际上,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可以优化新分配的阵列的冗余归零。您知道,所有Java对象必须在创建后立即归零。但是,只要编译器检测到数组在创建后立即被填充,它就可以消除归零,从而使结果代码更快。

P.S。 @ assylias&#39;基准测试很好,但它也受到System.arraycopy没有大型数组内在化的事实的影响。如果小型数组arrayCopy基准测试每秒被调用多次,JIT认为它很热并且优化得很好。但是对于大型数组,每次迭代都会更长,因此每秒的迭代次数要少得多,并且JIT不会将arrayCopy视为热点。

答案 1 :(得分:7)

使用jmh,我得到下表所示的结果(大小是数组的大小,得分是以微秒为单位的时间,错误显示置信区间为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非常擅长优化这样一个简单的循环。对于较小的数组,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;
  }
}