2016-10-27 40 views
9

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

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

+0

我猜测编译器超过了你,并且把你的手动拷贝变成了一个arraycopy,但没有Math.min,也没有额外的函数间接寻址。此外,也许可以交换订单几次并记录GC呼叫。 – TWT

回答

25

其实,热点编译器是足够聪明的展开和矢量化手动复制循环 - 这就是为什么结果代码似乎是很好的优化。

为什么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的基准是好的,但它也遭受System.arraycopy不是内在的大数组事实。如果是小型阵列arrayCopy基准被称为每秒多次,JIT认为它很热并且优化得很好。但对于大型数组,每次迭代更长,因此每秒迭代次数少得多,JIT不会将arrayCopy视为热门。

+4

这让我想起了古典[*“加速循环”*](http://thedailywtf.com/articles/The-Speedup-Loop)的故事:) – apangin

+0

谢谢。我认为这可能与JIT编译器有关,但不知道为什么。我试图强制优化(进行热身阶段),但显然很差。 –

6

使用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; 
    } 
} 
+0

感谢您的回复。我也想让这个新阵列更大,这就是为什么我没有使用克隆。 –