Почему Arrays.copyOf в 2 раза быстрее, чем System.arraycopy для небольших массивов?

Я недавно играл с некоторыми тестами и нашел очень интересные результаты, которые я не могу сейчас объяснить. Вот эталон:

@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, batchSize = 1000)
@Measurement(iterations = 10, time = 1, batchSize = 1000)
public class ArrayCopy {

    @Param({"1","5","10","100", "1000"})
    private int size;
    private int[] ar;

    @Setup
    public void setup() {
        ar = new int[size];
        for (int i = 0; i < size; i++) {
            ar[i] = i;
        }
    }

    @Benchmark
    public int[] SystemArrayCopy() {
        final int length = size;
        int[] result = new int[length];
        System.arraycopy(ar, 0, result, 0, length);
        return result;
    }

    @Benchmark
    public int[] javaArrayCopy() {
        final int length = size;
        int[] result = new int[length];
        for (int i = 0; i < length; i++) {
            result[i] = ar[i];
        }
        return result;
    }

    @Benchmark
    public int[] arraysCopyOf() {
        final int length = size;
        return Arrays.copyOf(ar, length);
    }

}

Результат:

Benchmark                  (size)   Mode  Cnt       Score      Error  Units
ArrayCopy.SystemArrayCopy       1  thrpt   10   52533.503 ± 2938.553  ops/s
ArrayCopy.SystemArrayCopy       5  thrpt   10   52518.875 ± 4973.229  ops/s
ArrayCopy.SystemArrayCopy      10  thrpt   10   53527.400 ± 4291.669  ops/s
ArrayCopy.SystemArrayCopy     100  thrpt   10   18948.334 ±  929.156  ops/s
ArrayCopy.SystemArrayCopy    1000  thrpt   10    2782.739 ±  184.484  ops/s
ArrayCopy.arraysCopyOf          1  thrpt   10  111665.763 ± 8928.007  ops/s
ArrayCopy.arraysCopyOf          5  thrpt   10   97358.978 ± 5457.597  ops/s
ArrayCopy.arraysCopyOf         10  thrpt   10   93523.975 ± 9282.989  ops/s
ArrayCopy.arraysCopyOf        100  thrpt   10   19716.960 ±  728.051  ops/s
ArrayCopy.arraysCopyOf       1000  thrpt   10    1897.061 ±  242.788  ops/s
ArrayCopy.javaArrayCopy         1  thrpt   10   58053.872 ± 4955.749  ops/s
ArrayCopy.javaArrayCopy         5  thrpt   10   49708.647 ± 3579.826  ops/s
ArrayCopy.javaArrayCopy        10  thrpt   10   48111.857 ± 4603.024  ops/s
ArrayCopy.javaArrayCopy       100  thrpt   10   18768.866 ±  445.238  ops/s
ArrayCopy.javaArrayCopy      1000  thrpt   10    2462.207 ±  126.549  ops/s

Итак, здесь есть две странные вещи:

  • Arrays.copyOf в 2 раза быстрее, чем System.arraycopy для небольших массивов (размер 1,5,10). Однако на большом массиве размером 1000 Arrays.copyOf становится почти в 2 раза медленнее. Я знаю, что оба метода являются внутренними, поэтому я ожидал такой же производительности. Откуда эта разница?
  • Копирование 1-элементного массива вручную выполняется быстрее, чем System.arraycopy. Мне непонятно почему. Кто-нибудь знает?

Версия VM: JDK 1.8.0_131, VM 25.131-b11


person Dmitriy Dumanskiy    schedule 11.06.2017    source источник
comment
Поскольку copyOf внутренне использует arraycopy, проблема в вашем тесте.   -  person Andreas    schedule 11.06.2017
comment
@Andreas Вы не правы. Arrays.copyOf является внутренним JVM. После JIT-компиляции метода Java-код в Arrays.java вообще не выполняется.   -  person apangin    schedule 11.06.2017
comment
@Andreas. Видите ли вы какую-нибудь конкретную проблему с тестом? Он разумно использует платформу JMH, чтобы избежать распространенных ошибок при тестировании.   -  person apangin    schedule 11.06.2017
comment
@apangin Это различие без разницы. То же самое относится к любому коду Java. Это не делает любой произвольный метод «внутренним» JVM.   -  person user207421    schedule 12.06.2017
comment
@EJP Хорошо, я перефразирую. JIT-компилятор не смотрит на байт-код Arrays.copyOf, потому что он внутренне знает, что должен делать этот метод.   -  person apangin    schedule 12.06.2017


Ответы (1)


Ваш SystemArrayCopy тест семантически не эквивалентен arraysCopyOf.

Будет, если заменить

    System.arraycopy(ar, 0, result, 0, length);

с участием

    System.arraycopy(ar, 0, result, 0, Math.min(ar.length, length));

С этим изменением производительность обоих тестов также станет схожей.

Почему тогда первый вариант медленнее?

  1. Не зная, как length относится к ar.length, JVM необходимо выполнить дополнительную проверку границ и быть готовым бросить IndexOutOfBoundsException, когда length > ar.length.
  2. Это также нарушает оптимизацию, чтобы исключить избыточное обнуление. Вы знаете, каждый выделенный массив должен быть инициализирован нулями. Однако JIT может избежать обнуления, если видит, что массив заполняется сразу после создания. Но -prof perfasm ясно показывает, что исходный SystemArrayCopy тест тратит значительное количество времени на очистку выделенного массива:

     0,84%    0x000000000365d35f: shr    $0x3,%rcx
     0,06%    0x000000000365d363: add    $0xfffffffffffffffe,%rcx
     0,69%    0x000000000365d367: xor    %rax,%rax
              0x000000000365d36a: shl    $0x3,%rcx
    21,02%    0x000000000365d36e: rep rex.W stos %al,%es:(%rdi)  ;*newarray
    

Копирование вручную стало быстрее для небольших массивов, потому что, в отличие от System.arraycopy, оно не выполняет никаких вызовов функций ВМ во время выполнения.

person apangin    schedule 11.06.2017
comment
Разве ar.length и length не одно и то же? Какой эффект имеет Math.min(ar.length, length)? - person Boann; 12.06.2017
comment
@Boann Они такие же, но JVM этого не знает. Math.min сообщает компилятору, что System.arraycopy никогда не выбросит IndexOutOfBoundsException. - person apangin; 12.06.2017
comment
Пожалуйста, объясни. Компилятор не знает System.arraycopy() из дыры в земле, кроме своего имени, вызывающей последовательности, результата и предложения throws (которого на самом деле у него нет). System.arraycopy() просто получает результат Math.min() в своем пятом аргументе и не знает, как он был получен. - person user207421; 12.06.2017
comment
@EJP Я не уверен, какой компилятор вы имеете в виду, но JIT-компилятор HotSpot (на самом деле, оба - C1 и C2) определенно знает о System.arraycopy() и преобразует вызов в график узлов IR. Затем анализ потока данных помогает уменьшить этот график. - person apangin; 12.06.2017
comment
@apangin, спасибо! Вы знаете, почему copyOf становится медленнее на больших массивах? - person Dmitriy Dumanskiy; 13.06.2017
comment
@DmitriyDumanskiy Это не так. Я не могу воспроизвести этот эффект. На всех протестированных мной системах arraysCopyOf постоянно быстрее, чем SystemArrayCopy. - person apangin; 13.06.2017
comment
Интересно. Я еще раз проверю. - person Dmitriy Dumanskiy; 13.06.2017
comment
@apangin «Я имею в виду компилятор» - это компилятор Java. Очевидно. То, что вы сбиваете с толку, теперь называете «JIT-компилятором HotSpot», использует терминологию «JIT», которая исчезла почти двадцать лет назад, в Java 1.3, и была заменена на HotSpot JVM. - person user207421; 15.06.2017
comment
@EJP Кто заставил этот термин исчезнуть? "JIT" по-прежнему официально используется на веб-сайте HotSpot, средство отслеживания ошибок, списки рассылки OpenJDK и в публичных презентациях инженерами Oracle. - person apangin; 15.06.2017
comment
@EJP JVM - это гораздо больше, чем просто компилятор. JIT-компилятор - лишь одна его часть. C1, C2 и Graal - это частные примеры JIT-компиляторов (также известных как динамические компиляторы), которые работают в HotSpot JVM. - person apangin; 15.06.2017