【问题标题】:System.arrayCopy is slowSystem.arrayCopy 很慢
【发布时间】:2016-10-27 13:56:43
【问题描述】:

我一直在尝试测量 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 由于它的诞生是复制数组的最佳方法,但我无法解释这个结果。

【问题讨论】:

  • 我猜编译器比你聪明,把你的手动拷贝变成了一个数组拷贝,但是没有 Math.min,也没有额外的函数间接。此外,可能会交换几次订单并记录 GC 调用。

标签: java performance arrays


【解决方案1】:

实际上,HotSpot 编译器足够智能,可以展开和矢量化手动复制循环 - 这就是结果代码看起来得到很好优化的原因。

那为什么System.arraycopy 更慢呢?它最初是一个native方法,你必须为native调用付费,直到编译器将其优化为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 的基准测试很好,但它也受到System.arraycopy 不适用于大型数组的事实的影响。在小型阵列arrayCopy 每秒调用多次基准的情况下,JIT 认为它很热并且优化得很好。但是对于大型数组,每次迭代都更长,因此每秒的迭代次数要少得多,并且 JIT 不会将 arrayCopy 视为热。

【讨论】:

  • 这让我想起了经典的"Speed-up Loop"故事:)
  • 谢谢。我认为这可能与 JIT 编译器有关,但不知道为什么。我试图强制优化(进入热身阶段),但显然很糟糕。
  • @apangin 感谢您提供的详细信息,如果我错了,请纠正我,根据我的理解,如果任何方法使用空 FOR LOOP,它会触发后沿计数器以进行触发器编译。这意味着在 Javac 过程中,方法快 1.5 倍意味着执行时间还是其他?我没明白这一点。
  • @ThangavelLoganathan 我说的是 JIT 编译,而不是 Javac。这里的“更快”意味着更短的执行时间。
【解决方案2】:

使用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 非常擅长优化这样一个简单的循环。对于较小的数组,arrayCopy 似乎更好(尽管差异很小)。

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


作为参考,基准代码,使用-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;
  }
}

【讨论】:

  • 感谢您的回复。我还想让新数组更大,所以我没有使用克隆。
猜你喜欢
  • 2019-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-22
  • 2011-03-08
  • 2013-02-23
  • 2015-12-24
  • 2020-10-23
相关资源
最近更新 更多