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