2016-10-27 72 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

感謝您的回覆。我也想讓這個新陣列更大,這就是爲什麼我沒有使用克隆。 –