System.arrayCopy很慢

Mic*_*áha 9 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" );
    }
}
Run Code Online (Sandbox Code Playgroud)

结果表明,手动复制的性能提高了约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
Run Code Online (Sandbox Code Playgroud)

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

apa*_*gin 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

PS @assylias的基准测试很好,但它也受到System.arraycopy了大型阵列没有内在化的影响.在小型阵列的情况下,arrayCopy基准测试每秒被调用多次,JIT认为它很热并且优化得很好.但是对于大型数组,每次迭代都会更长,因此每秒的迭代次数要少得多,并且JIT不会被arrayCopy视为热点.

  • 这让我想起了古典[*"Speed-up Loop"*](http://thedailywtf.com/articles/The-Speedup-Loop)的故事:) (4认同)

ass*_*ias 8

使用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
Run Code Online (Sandbox Code Playgroud)

实际上,对于大型数组,循环似乎比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;
  }
}
Run Code Online (Sandbox Code Playgroud)