Перейти к основному содержимому

Производительность System.arraycopy() по сравнению с Arrays.copyOf()

· 6 мин. чтения

1. Введение

В этом руководстве мы рассмотрим производительность двух методов Java: System.arraycopy() и Arrays.copyOf() . Сначала мы проанализируем их реализации. Во-вторых, мы запустим несколько тестов, чтобы сравнить их среднее время выполнения.

2. Производительность System.arraycopy()

System.arraycopy() копирует содержимое массива из исходного массива, начиная с указанной позиции, в указанную позицию в целевом массиве. Кроме того, перед копированием JVM проверяет, совпадают ли исходный и конечный типы.

При оценке производительности System.arraycopy() нужно помнить, что это нативный метод. Нативные методы реализованы в платформенно-зависимом коде (обычно C) и доступны через вызовы JNI.

Поскольку нативные методы уже скомпилированы для конкретной архитектуры, мы не можем точно оценить сложность выполнения. Более того, их сложность может различаться между платформами. Мы можем быть уверены, что наихудший сценарий — O(N) . Однако процессор может копировать смежные блоки памяти по одному блоку за раз ( memcpy() в C), поэтому фактические результаты могут быть лучше.

Мы можем просмотреть только подпись System.arraycopy() :

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

3. Производительность Arrays.copyOf()

Arrays.copyOf() предлагает дополнительную функциональность поверх того, что реализует System.arraycopy() . В то время как System.arraycopy() просто копирует значения из исходного массива в место назначения, Arrays.copyOf() также создает новый массив . При необходимости он обрежет или дополнит содержимое.

Второе отличие состоит в том, что новый массив может быть другого типа, чем исходный массив. В этом случае JVM будет использовать отражение, что увеличивает нагрузку на производительность .

При вызове с массивом объектов copyOf() вызовет отражающий метод Array.newInstance() :

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}

Однако при вызове с примитивами в качестве параметров для создания целевого массива не требуется отражение:

public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}

Мы можем ясно видеть, что в настоящее время реализация Arrays.copyOf() вызывает System.arraycopy() . В результате выполнение во время выполнения должно быть аналогичным. Чтобы подтвердить наше подозрение, мы проверим вышеуказанные методы как с примитивами, так и с объектами в качестве параметров.

4. Бенчмарк кода

Давайте проверим, какой метод копирования быстрее, на реальном тесте. Для этого мы будем использовать JMH (Java Microbenchmark Harness). Мы создадим простой тест, в котором мы будем копировать значения из одного массива в другой, используя как System.arraycopy() , так и Arrays.copyOf() .

Мы создадим два тестовых класса. В одном тестовом классе мы будем тестировать примитивы, а во втором — объекты . Конфигурация эталона будет одинаковой в обоих случаях.

4.1. Эталонная конфигурация

Во-первых, давайте определим параметры нашего теста:

@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 10)
@Fork(1)
@Measurement(iterations = 100)

Здесь мы указываем, что хотим запустить наш тест только один раз, с 10 итерациями прогрева и 100 итерациями измерения. Более того, мы хотели бы рассчитать среднее время выполнения и собрать результаты в наносекундах. Для получения точных результатов важно выполнить не менее пяти итераций прогрева.

4.2. Настройка параметров

Мы должны быть уверены, что измеряем только время, затрачиваемое на выполнение метода, а не на создание массива. Для этого мы инициализируем исходный массив на этапе настройки эталонного теста. Рекомендуется запустить тест как с большими, так и с маленькими числами.

В методе настройки мы просто инициализируем массив случайными параметрами. Во-первых, мы определяем настройку эталона для примитивов:

public class PrimitivesCopyBenchmark {

@Param({ "10", "1000000" })
public int SIZE;

int[] src;

@Setup
public void setup() {
Random r = new Random();
src = new int[SIZE];

for (int i = 0; i < SIZE; i++) {
src[i] = r.nextInt();
}
}
}

Та же установка следует для эталонного теста объектов:

public class ObjectsCopyBenchmark {

@Param({ "10", "1000000" })
public int SIZE;
Integer[] src;

@Setup
public void setup() {
Random r = new Random();
src = new Integer[SIZE];

for (int i = 0; i < SIZE; i++) {
src[i] = r.nextInt();
}
}
}

4.3. Тесты

Мы определяем два теста, которые будут выполнять операции копирования. Сначала мы вызовем System.arraycopy() :

@Benchmark
public Integer[] systemArrayCopyBenchmark() {
Integer[] target = new Integer[SIZE];
System.arraycopy(src, 0, target, 0, SIZE);
return target;
}

Чтобы сделать оба теста эквивалентными, мы включили в тест создание целевого массива.

Во-вторых, мы измерим производительность Arrays.copyOf() :

@Benchmark
public Integer[] arraysCopyOfBenchmark() {
return Arrays.copyOf(src, SIZE);
}

4.4. Полученные результаты

После запуска нашего теста давайте посмотрим на результаты:

Benchmark                                          (SIZE)  Mode  Cnt        Score       Error  Units
ObjectsCopyBenchmark.arraysCopyOfBenchmark 10 avgt 100 8.535 ± 0.006 ns/op
ObjectsCopyBenchmark.arraysCopyOfBenchmark 1000000 avgt 100 2831316.981 ± 15956.082 ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark 10 avgt 100 9.278 ± 0.005 ns/op
ObjectsCopyBenchmark.systemArrayCopyBenchmark 1000000 avgt 100 2826917.513 ± 15585.400 ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark 10 avgt 100 9.172 ± 0.008 ns/op
PrimitivesCopyBenchmark.arraysCopyOfBenchmark 1000000 avgt 100 476395.127 ± 310.189 ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark 10 avgt 100 8.952 ± 0.004 ns/op
PrimitivesCopyBenchmark.systemArrayCopyBenchmark 1000000 avgt 100 475088.291 ± 726.416 ns/op

Как мы видим, производительность System.arraycopy() и Arrays.copyOf() различается в зависимости от диапазона ошибки измерения как для примитивов, так и для объектов Integer . Это неудивительно, учитывая тот факт, что Arrays.copyOf() использует System.arraycopy() под капотом. Поскольку мы использовали два примитивных массива int , рефлексивные вызовы не делались.

Мы должны помнить, что JMH дает только приблизительную оценку времени выполнения , и результаты могут различаться между машинами и JVM.

5. Внутренние кандидаты

Стоит отметить, что в HotSpot JVM 16 и Arrays.copyOf() , и System.arraycopy() помечены как @IntrinsicCandidate . Эта аннотация означает, что виртуальная машина HotSpot может заменить аннотированный метод более быстрым низкоуровневым кодом.

Компилятор JIT может (для некоторых или всех архитектур) заменить внутренние методы машинно-зависимыми, значительно оптимизированными инструкциями. Поскольку нативные методы являются черным ящиком для компилятора со значительными накладными расходами на вызовы, производительность обоих методов может быть выше. Опять же, такой прирост производительности не гарантируется.

6. Заключение

В этом примере мы рассмотрели производительность System.arraycopy( ) и Arrays.copyOf( ). Сначала мы проанализировали исходный код обоих методов. Во-вторых, мы настроили пример эталонного теста для измерения их среднего времени выполнения.

В результате мы подтвердили нашу теорию о том, что, поскольку Arrays.copyOf() использует System.arraycopy() , производительность обоих методов очень похожа.

Как обычно, примеры, использованные в этой статье, доступны на GitHub .