System.arrayCopy работает медленно
Я пытался измерить производительность 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
из-за его рождества - лучший возможный способ скопировать массив, но я не могу объяснить этот результат.
2 ответа
На самом деле, компилятор 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 должны быть обнулены сразу после создания. Но поскольку компилятор обнаруживает, что массив заполнен сразу после создания, он может исключить обнуление, тем самым делая код результата еще быстрее.
ТестPS @assylias хорош, но он также страдает от того факта, что System.arraycopy
не присущ для больших массивов. В случае небольших массивов arrayCopy
бенчмарк вызывается много раз в секунду, JIT считает его горячим и хорошо оптимизирует. Но для больших массивов каждая итерация длиннее, поэтому итераций в секунду намного меньше, и JIT не обрабатывает arrayCopy
выстрел.
Используя 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;
}
}