System.arrayCopy медленный
Я пытаюсь измерить производительность System.arrayCopy vs 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
из-за его nativity - лучший способ скопировать массив, но я не могу объяснить этот результат.
Ответы
Ответ 1
Собственно, компилятор 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;
}
Пустой цикл запускает переполнение счетчика backedge, что, в свою очередь, вызывает компиляцию метода enlarge
. Пустой цикл затем исключается из скомпилированного кода, поэтому он безвреден. Теперь метод enlarge
получает примерно в 1,5 раза быстрее, чем ручной цикл!
Важно, что System.arraycopy
сразу следует за new int[]
. В этом случае HotSpot может оптимизировать избыточное обнуление нового выделенного массива. Знаете, все объекты Java должны быть обнулены сразу после создания. Но поскольку компилятор обнаруживает, что массив заполняется сразу после создания, он может исключить обнуление, тем самым делая код результата еще быстрее.
P.S. Тест @assylias хорош, но он также страдает тем, что System.arraycopy
не является встроенным для больших массивов. В случае небольших массивов arrayCopy
критерий называется много раз в секунду, JIT считает его горячим и хорошо оптимизируется. Но для больших массивов каждая итерация длиннее, поэтому в секунду происходит намного меньше итераций, а JIT не обрабатывает arrayCopy
как горячую.
Ответ 2
Используя 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;
}
}