Подтвердить что ты не робот

Почему clone() - лучший способ для копирования массивов?

Это позор для меня, но я этого не знал:

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

как заявляет Джош Блох в этом блоге: http://www.artima.com/intv/bloch13.html

Я всегда использовал System.arraycopy(...). Оба подхода являются родными, поэтому, возможно, не углубляясь в источники библиотек, я не могу понять, почему это так.

Мой вопрос прост:  почему это самый быстрый способ? В чем разница с System.arraycopy? Разница объясняется here, но она не отвечает на вопрос, почему Джош Блох считает clone() самым быстрым способом.

4b9b3361

Ответ 1

Я хотел бы рассказать о том, почему clone() - это самый быстрый способ скопировать массив, чем System.arraycopy(..) или другие:

1. clone() не нужно выполнять проверку типов перед копированием исходного массива в пункт назначения, как указано здесь. Он просто просто выделяет новое пространство памяти и присваивает ему объекты. С другой стороны, System.arraycopy(..) проверяет тип и затем копирует массив.

2. clone() также нарушает оптимизацию, чтобы исключить избыточное обнуление. Как вы знаете, каждый выделенный массив в Java должен быть инициализирован с помощью 0s или соответствующих значений по умолчанию. Однако JIT может избежать обнуления этого массива, если видит, что массив заполняется сразу после создания. Это делает его определенно быстрее по сравнению с изменением значений копирования с существующими 0s или соответствующими значениями по умолчанию. При использовании 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 BenchmarkTests {

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

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

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


    @Benchmark
    public int[] arrayClone() {
        return original.clone();
    }

}

Вывод:

Benchmark                        (size)   Mode  Cnt       Score      Error  Units
ArrayCopy.SystemArrayCopy            1  thrpt   10   26324.251 ± 1532.265  ops/s
ArrayCopy.SystemArrayCopy            5  thrpt   10   26435.562 ± 2537.114  ops/s
ArrayCopy.SystemArrayCopy           10  thrpt   10   27262.200 ± 2145.334  ops/s
ArrayCopy.SystemArrayCopy          100  thrpt   10   10524.117 ±  474.325  ops/s
ArrayCopy.SystemArrayCopy         1000  thrpt   10     984.213 ±  121.934  ops/s
ArrayCopy.arrayClone                 1  thrpt   10   55832.672 ± 4521.112  ops/s
ArrayCopy.arrayClone                 5  thrpt   10   48174.496 ± 2728.928  ops/s
ArrayCopy.arrayClone                10  thrpt   10   46267.482 ± 4641.747  ops/s
ArrayCopy.arrayClone               100  thrpt   10   19837.480 ±  364.156  ops/s
ArrayCopy.arrayClone              1000  thrpt   10    1841.145 ±  110.322  ops/s

В соответствии с выводами я получаю, что clone почти в два раза быстрее от System.arraycopy(..)

3. Кроме того, использование метода ручного копирования, такого как clone(), приводит к более быстрому выводу, поскольку ему не нужно делать какие-либо вызовы VM (в отличие от System.arraycopy()).

Ответ 2

С одной стороны, clone() не нужно выполнять проверку типа, которую выполняет System.arraycopy().

Ответ 3

Я хочу исправить и дополнить предыдущие ответы.

  • Object.clone использует неконтролируемую реализацию System.arraycopy для массивов;
  • Основным улучшением производительности Object.clone является инициализация RAW-памяти напрямую. В случае System.arraycopy он также пытается комбинировать инициализацию массива с операцией копирования, как мы видим в исходном коде, но также выполняет различные дополнительные проверки для этого, в отличие от Object.clone. Если вы просто отключите эту функцию (см. Ниже), производительность будет очень близкой (в частности, на моем оборудовании).
  • Еще одна интересная вещь о Young vs Old Gen. Если исходный массив выровнен и внутри старого Gen, оба метода имеют близкую производительность.
  • Когда мы копируем примитивные массивы, System.arraycopy всегда использует generate_unchecked_arraycopy.
  • Это зависит от аппаратных/OS-зависимых реализаций, поэтому не доверяйте критериям и предположениям, проверяйте сами.

Объяснение

Прежде всего, метод клонирования и System.arraycopy являются внутренними. Object.clone и System.arraycopy используют generate_unchecked_arraycopy. И если мы пойдем глубже, мы увидим, что после этого HotSpot выбирает конкретную реализацию, зависящую от ОС и т.д.

Longly. Посмотрите код Hotspot. Прежде всего, мы увидим, что Object.clone(LibraryCallKit:: inline_native_clone) использует generate_arraycopy, который используется для System.arraycopy в случае -XX: -ReduceInitialCardMarks. В противном случае он выполняет LibraryCallKit:: copy_to_clone, который инициализирует новый массив в RAW-памяти (если -XX: + ReduceBulkZeroing, который включен по умолчанию). В отличие от System.arraycopy напрямую использует generate_arraycopy, попробуйте проверить ReduceBulkZeroing (и многие другие случаи) и также устраните обнуление массива с упомянутыми дополнительными проверками, а также проведет дополнительные проверки, чтобы убедиться, что все элементы инициализированы, в отличие от Object.clone. Наконец, в лучшем случае оба из них используют generate_unchecked_arraycopy.

Ниже я показываю некоторые ориентиры, чтобы увидеть этот эффект на практике:

  • Первый - просто простой тест, единственное отличие от предыдущего ответа, что массивы не отсортированы; Мы видим, что arraycopy медленнее (но не два раза), результаты - https://pastebin.com/ny56Ag1z;
  • Во-вторых, я добавляю параметр -XX: -ReduceBulkZeroing, и теперь я вижу, что производительность обоих методов очень близка. Результаты - https://pastebin.com/ZDAeQWwx;
  • Я также предполагаю, что мы будем иметь разницу между Old/Young, из-за выравнивания массивов (это особенность Java GC, когда мы вызываем GC, выравнивание массивов изменяется, его легко наблюдать, используя JOL). Я был удивлен, что производительность стала такой же, как правило, и понижением для обоих методов. Результаты - https://pastebin.com/bTt5SJ8r. Для кого, кто верит в конкретные числа, пропускная способность System.arraycopy лучше, чем Object.clone.

Первый контрольный показатель:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CloneVsArraycopy {

    @Param({"10", "1000", "100000"})
    int size;

    int[] source;

    @Setup(Level.Invocation)
    public void setup() {
        source = create(size);
    }

    @Benchmark
    public int[] clone(CloneVsArraycopy cloneVsArraycopy) {
        return cloneVsArraycopy.source.clone();
    }

    @Benchmark
    public int[] arraycopy(CloneVsArraycopy cloneVsArraycopy) {
        int[] dest = new int[cloneVsArraycopy.size];
        System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length);
        return dest;
    }

    public static void main(String[] args) throws Exception {
        new Runner(new OptionsBuilder()
                .include(CloneVsArraycopy.class.getSimpleName())
                .warmupIterations(20)
                .measurementIterations(20)
                .forks(20)
                .build()).run();
    }

    private static int[] create(int size) {
        int[] a = new int[size];
        for (int i = 0; i < a.length; i++) {
            a[i] = ThreadLocalRandom.current().nextInt();
        }
        return a;
    }

}

Запуск этого теста на моем ПК, я получил это - https://pastebin.com/ny56Ag1z. Разница не такая большая, но все же существует.

Второй критерий я добавлю только один параметр -XX: -ReduceBulkZeroing и получил эти результаты https://pastebin.com/ZDAeQWwx, Нет, мы видим, что для Young Gen разница намного меньше.

В третьем тесте я изменил только метод настройки и включил опцию ReduceBulkZeroing:

@Setup(Level.Invocation)
public void setup() {
    source = create(size);
    // try to move to old gen/align array
    for (int i = 0; i < 10; ++i) {
        System.gc();
    }
}

Разница намного меньше (возможно, в интервале ошибок) - https://pastebin.com/bTt5SJ8r.

Отказ от ответственности

Это также может быть неправильно. Вы должны сами проверить.

Кроме того,

Я думаю, интересно посмотреть на процесс тестирования:

# Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.arraycopy
# Parameters: (size = 50000)

# Run progress: 0,00% complete, ETA 00:07:30
# Fork: 1 of 5
# Warmup Iteration   1: 8,870 ops/ms
# Warmup Iteration   2: 10,912 ops/ms
# Warmup Iteration   3: 16,417 ops/ms <- Hooray!
# Warmup Iteration   4: 17,924 ops/ms <- Hooray!
# Warmup Iteration   5: 17,321 ops/ms <- Hooray!
# Warmup Iteration   6: 16,628 ops/ms <- What!
# Warmup Iteration   7: 14,286 ops/ms <- No, stop, why!
# Warmup Iteration   8: 13,928 ops/ms <- Are you kidding me?
# Warmup Iteration   9: 13,337 ops/ms <- pff
# Warmup Iteration  10: 13,499 ops/ms
Iteration   1: 13,873 ops/ms
Iteration   2: 16,177 ops/ms
Iteration   3: 14,265 ops/ms
Iteration   4: 13,338 ops/ms
Iteration   5: 15,496 ops/ms

Для объекта Object.clone

# Benchmark: org.egorlitvinenko.arrays.CloneVsArraycopy.clone
# Parameters: (size = 50000)

# Run progress: 0,00% complete, ETA 00:03:45
# Fork: 1 of 5
# Warmup Iteration   1: 8,761 ops/ms
# Warmup Iteration   2: 12,673 ops/ms
# Warmup Iteration   3: 20,008 ops/ms
# Warmup Iteration   4: 20,340 ops/ms
# Warmup Iteration   5: 20,112 ops/ms
# Warmup Iteration   6: 20,061 ops/ms
# Warmup Iteration   7: 19,492 ops/ms
# Warmup Iteration   8: 18,862 ops/ms
# Warmup Iteration   9: 19,562 ops/ms
# Warmup Iteration  10: 18,786 ops/ms

Мы можем наблюдать снижение производительности здесь для System.arraycopy. Я видел подобную картину для Streams, и в компиляторах была ошибка. Полагаю, это тоже может быть ошибкой в ​​компиляторах. Во всяком случае, странно, что после 3-х разгонов производительность понижается.

UPDATE

Что такое typechecking

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CloneVsArraycopyObject {

    @Param({"100"})
    int size;

    AtomicLong[] source;

    @Setup(Level.Invocation)
    public void setup() {
        source = create(size);
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public AtomicLong[] clone(CloneVsArraycopyObject cloneVsArraycopy) {
        return cloneVsArraycopy.source.clone();
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public AtomicLong[] arraycopy(CloneVsArraycopyObject cloneVsArraycopy) {
        AtomicLong[] dest = new AtomicLong[cloneVsArraycopy.size];
        System.arraycopy(cloneVsArraycopy.source, 0, dest, 0, dest.length);
        return dest;
    }

    public static void main(String[] args) throws Exception {
        new Runner(new OptionsBuilder()
                .include(CloneVsArraycopyObject.class.getSimpleName())
                .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining", "-XX:-ReduceBulkZeroing")
                .warmupIterations(10)
                .measurementIterations(5)
                .forks(5)
                .build())
                .run();
    }

    private static AtomicLong[] create(int size) {
        AtomicLong[] a = new AtomicLong[size];
        for (int i = 0; i < a.length; i++) {
            a[i] = new AtomicLong(ThreadLocalRandom.current().nextLong());
        }
        return a;
    }

}

Разница не наблюдается - https://pastebin.com/ufxCZVaC. Я полагаю, что объяснение простое, так как System.arraycopy является горячим внутренним в этом случае, реальная реализация будет просто встроена без каких-либо типов и т.д.

Примечание

Я согласился с Radiodef, вам было бы интересно прочитать сообщение в блоге, автор этого блога является создателем (или одним из создателей) JMH.

Ответ 4

Что касается копирования, то System.arrayCopy является самым быстрым, а затем и сейчас.

  • System.arrayCopy не создает новый массив и не может быть избит с использованием скорости копирования.
  • Arrays.copyOf просто создает массив и вызывает arrayCopy. Удобство.
  • Array.clone является высокоэффективным, но ему нужно очистить скопированные данные до всех кеш-памяти процессора.

Если вы можете использовать код для повторного использования массива с помощью arrayCopy, перейдите к нему. В противном случае я лично рекомендую copyOf с учетом восходящего тренда ядер процессора и потому, что клон считается старым и проблематичный в целом - главный пункт блог Джоша Блоха, который начал этот вопрос.

В отличие от общих мыслей, фактические контуры копирования (тип проверен или нет) не являются байт-кодом Java и не оптимизируются по точкам доступа. Циклы кодируются в С++ и представляют собой реализацию jvm на уровне низкого уровня.


Длинный ответ:

Этот ответ основан на ссылке на исходный код OpenJDK 8 и который, насколько я знаю, должен быть тем же самым для Sun.

Копия массива, возможно, более сложная, чем большинство людей думают. На уровне кода C его можно разделить на три случая:

Таким образом, абсолютная скорость копирования массива сильно зависит от типа массива. Однако относительная скорость трех методов клонирования не возникает, поскольку все они решаются на один и тот же цикл копирования, встроенный цикл С++ или сборки. Таким образом, разная скорость связана главным образом с накладными расходами и другими факторами.

  • System.arrayCopy - это, в основном, проверки типов и длины, а затем прямо в контур копирования, В моих собственных тестах arrayCopy всегда быстрее других двух методов, значительно превышающих допустимые пределы ошибок.

  • Arrays.copyOf просто вызывает System.arrayCopy - после создания нового массива. Обратите внимание, что он не вызывает Array.clone. В отличие от Radiodef comment, нет никаких признаков того, что Java 8 будет обходить инициализацию нуля.

  • Array.clone интересен. Он напрямую вызывает распределение кучи и цикл копирования с минимальными проверками. Таким образом, его создание массива должно быть быстрее, чем Arrays.copyOf, а его копия выполняется быстрее, чем System.arrayCopy, если не быстрее.

Но в моих тестах Array.clone немного медленнее, чем copyOf.

Я подозреваю, что из-за барьер памяти после копии. Как и конструктор, clone будет следить за тем, чтобы скопированные данные были видны для всех потоков, которые не выполняются ни System.arrayCopy, ни Array.copyOf. Это означает, что Array.clone необходимо потратить время на ожидание кэша ЦП для обновления.

Если это так, результат Array.clone vs Arrays.copyOf зависит от того, будет ли кеш-флеш clone быстрее, чем накладные расходы copyOf, и должен быть зависимым от платформы.

Кроме этого, поскольку клонирование всегда приводит к массиву того же типа, все три метода в конечном итоге используют один и тот же цикл копирования.

Если вы хотите только скопировать, arrayCopy всегда быстрее, просто потому, что он не создает новый массив. В остальном, если список рассылки java - это что-то, что нужно сделать, выбор между Arrays.copyOf и Array.clone должен быть в основном вопросом вкуса.


Результат и код теста jmh ниже.

  • Односторонние тесты возвращают скопированный массив.
  • Двухсторонние тесты перезаписывают источник копирования, который заставляет следующий клон копировать "новые" данные.
  • NoClone не клонирует ничего и является мерилом, чтобы убедиться, что скорость выше.

Как указано, Clone и CopyOf - это близкая гонка, и ваш пробег может меняться.

/* # Run complete. Total time: 00:06:44

Benchmark                               Mode  Cnt          Score         Error  Units
MyBenchmark.ArrayCloneByteOneWay       thrpt   20    1048588.503 ±    2608.862  ops/s
MyBenchmark.ArrayCloneByteTwoWay       thrpt   20     523782.848 ±    1613.823  ops/s
MyBenchmark.ArrayCloneObjOneWay        thrpt   20     260903.006 ±    1311.827  ops/s
MyBenchmark.ArrayCloneObjTwoWay        thrpt   20     129448.639 ±    1179.122  ops/s
MyBenchmark.ArraysCopyOfByteOneWay     thrpt   20    1065995.804 ±    2197.919  ops/s
MyBenchmark.ArraysCopyOfByteTwoWay     thrpt   20     533025.610 ±    2831.955  ops/s
MyBenchmark.ArraysCopyOfObjOneWay      thrpt   20     266134.565 ±    1536.756  ops/s
MyBenchmark.ArraysCopyOfObjTwoWay      thrpt   20     130821.380 ±     274.325  ops/s
MyBenchmark.NoClone                    thrpt   20  308776528.157 ± 2546848.128  ops/s
MyBenchmark.SystemArrayCopyByteOneWay  thrpt   20    1232733.367 ±    8439.409  ops/s
MyBenchmark.SystemArrayCopyByteTwoWay  thrpt   20     859387.983 ±    1919.359  ops/s
MyBenchmark.SystemArrayCopyObjOneWay   thrpt   20     239532.442 ±     775.193  ops/s
MyBenchmark.SystemArrayCopyObjTwoWay   thrpt   20     167235.661 ±     503.141  ops/s
*/

import java.util.Arrays;
import java.util.Random;
import org.openjdk.jmh.annotations.*;

@Fork(2) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 10, time = 1)
public class Q46230557 {
   private static final int ARRAY_SIZE = 8192;

   @State(Scope.Thread) public static class Data {
      public byte[] bytes = new byte[ ARRAY_SIZE ];
      public Object[] objs = new Object[ ARRAY_SIZE ];
      @Setup public void setup() {
         final Random RNG = new Random();
         RNG.nextBytes( bytes );
         for ( int i = 0 ; i < ARRAY_SIZE ; i++ )
            objs[i] = RNG.nextInt();
      }
   }

   @Benchmark public byte[] NoClone( final Data data ) {
      return data.bytes;
   }

   @Benchmark public byte[] SystemArrayCopyByteOneWay( final Data data ) {
      final byte[] dest = new byte[ ARRAY_SIZE ];
      System.arraycopy( data.bytes, 0, dest, 0, ARRAY_SIZE );
      return dest;
   }

   @Benchmark public byte[] SystemArrayCopyByteTwoWay( final Data data ) {
      final byte[] buf = new byte[ ARRAY_SIZE ];
      System.arraycopy( data.bytes, 0, buf, 0, ARRAY_SIZE );
      System.arraycopy( buf, 0, data.bytes, 0, ARRAY_SIZE );
      return data.bytes;
   }

   @Benchmark public byte[] ArraysCopyOfByteOneWay( final Data data ) {
      return Arrays.copyOf( data.bytes, ARRAY_SIZE );
   }

   @Benchmark public byte[] ArraysCopyOfByteTwoWay( final Data data ) {
      final byte[] buf = Arrays.copyOf( data.bytes, ARRAY_SIZE );
      return data.bytes = Arrays.copyOf( buf, ARRAY_SIZE );
   }

   @Benchmark public byte[] ArrayCloneByteOneWay( final Data data ) {
      return data.bytes.clone();
   }

   @Benchmark public byte[] ArrayCloneByteTwoWay( final Data data ) {
      final byte[] buf = data.bytes.clone();
      return data.bytes = buf.clone();
   }

   @Benchmark public Object[] SystemArrayCopyObjOneWay( final Data data ) {
      final Object[] dest = new Object[ ARRAY_SIZE ];
      System.arraycopy( data.objs, 0, dest, 0, ARRAY_SIZE );
      return dest;
   }

   @Benchmark public Object[] SystemArrayCopyObjTwoWay( final Data data ) {
      final Object[] buf = new Object[ ARRAY_SIZE ];
      System.arraycopy( data.objs, 0, buf, 0, ARRAY_SIZE );
      System.arraycopy( buf, 0, data.objs, 0, ARRAY_SIZE );
      return data.objs;
   }

   @Benchmark public Object[] ArraysCopyOfObjOneWay( final Data data ) {
      return Arrays.copyOf( data.objs, ARRAY_SIZE );
   }

   @Benchmark public Object[] ArraysCopyOfObjTwoWay( final Data data ) {
      final Object[] buf = Arrays.copyOf( data.objs, ARRAY_SIZE );
      return data.objs = Arrays.copyOf( buf, ARRAY_SIZE );
   }

   @Benchmark public Object[] ArrayCloneObjOneWay( final Data data ) {
      return data.objs.clone();
   }

   @Benchmark public Object[] ArrayCloneObjTwoWay( final Data data ) {
      final Object[] buf = data.objs.clone();
      return data.objs = buf.clone();
   }
}

Ответ 5

Разница в производительности возникает из-за пропуска шага, где массив обнуляется.

public static int[] copyUsingArraycopy(int[] original)
{
    // Memory is allocated and zeroed out
    int[] copy = new int[original.Length];
    // Memory is copied
    System.arraycopy(original, 0, copy, 0, original.length);
}

public static int[] copyUsingClone(int[] original)
{
    // Memory is allocated, but not zeroed out
    // Unitialized memory is then copied into
    return (int[])original.clone();
}

Однако в тех случаях, когда производительность копирования массива имеет существенное значение, обычно лучше использовать двойную буферизацию.

int[] backBuffer = new int[BUFFER_SIZE];
int[] frontBuffer = new int[BUFFER_SIZE];

...

// Swap buffers
int[] temp = frontBuffer;
frontBuffer = backBuffer;
backBuffer = temp;
System.arraycopy(frontBuffer, 0, backBuffer, 0, BUFFER_SIZE);