web-dev-qa-db-fra.com

Pourquoi clone () est le meilleur moyen de copier des tableaux?

C'est dommage pour moi, mais je ne le savais pas: 

Vous devez utiliser clone pour copier des tableaux, car c’est généralement le moyen le plus rapide de le faire.

comme le dit Josh Bloch dans ce blog: http://www.artima.com/intv/bloch13.html

J'ai toujours utilisé System.arraycopy(...). Les deux approches sont natives, donc probablement sans aller plus loin dans les sources des bibliothèques, je ne peux pas comprendre pourquoi il en est ainsi. 

Ma question est simple: pourquoi est-ce le le plus rapide? Quelle est la différence avec System.arraycopy? La différence est expliquée ici , mais cela ne répond pas à la question de savoir pourquoi Josh Bloch considère clone() comme le moyen le plus rapide.

28
Andremoniy

Je voudrais expliquer pourquoi clone() est le moyen le plus rapide de copier un tableau par rapport à System.arraycopy(..) ou à d’autres:

1.clone() n'a pas besoin de vérifier le typage avant de copier un tableau source dans le tableau de destination comme prévu ici . Il s’agit simplement d’allouer un nouvel espace mémoire et d’y assigner les objets. Par ailleurs, System.arraycopy(..) vérifie le type, puis copie un tableau.

2.clone() interrompt également l'optimisation pour éliminer la remise à zéro redondante. Comme vous le savez, chaque tableau alloué en Java doit être initialisé avec 0s ou des valeurs par défaut respectives. Cependant, JIT peut éviter de mettre à zéro ce tableau s'il constate que le tableau est rempli juste après la création. Cela le rend nettement plus rapide par rapport à la modification des valeurs de copie avec le 0s existant ou les valeurs par défaut respectives. Lorsque vous utilisez System.arraycopy(..), vous perdez beaucoup de temps à nettoyer et à copier le tableau initialisé. Pour ce faire, j'ai effectué certains tests de référence.

@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();
    }

}

Sortie:

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

D'après les résultats obtenus, clone est presque deux fois plus rapide que System.arraycopy(..)

3. De plus, l'utilisation d'une méthode de copie manuelle telle que clone() permet d'obtenir une sortie plus rapide, car elle ne nécessite aucun appel VM (contrairement à System.arraycopy()).

20
procrastinator

D'une part, clone() n'a pas à faire la vérification typographique que System.arraycopy() fait.

4
user207421

Je veux corriger et compléter les réponses précédentes. 

  1. Object.clone utilise une implémentation non contrôlée de System.arraycopy pour les tableaux.
  2. L’amélioration principale des performances d’Object.clone, c’est l’initialisation directe de la mémoire RAW. Dans le cas de System.arraycopy, il tente également de combiner l'initialisation d'un tableau avec l'opération de copie, comme on peut le voir dans le code source, mais il effectue également différentes vérifications supplémentaires à la différence de Object.clone. Si vous désactivez simplement cette fonctionnalité (voir ci-dessous), les performances seront alors très proches (en particulier sur mon matériel).
  3. Une autre chose intéressante concerne Young vs Old Gen. Dans le cas où le tableau source est aligné et dans Old Gen, les deux méthodes ont des performances proches.
  4. Lorsque nous copions des tableaux primitifs, System.arraycopy utilise toujours generate_unchecked_arraycopy.
  5. Cela dépend des implémentations dépendantes du matériel/du système d'exploitation. Ne faites donc pas confiance aux tests de performance et aux hypothèses, vérifiez par vous-même.

Explication

Tout d'abord, la méthode clone et System.arraycopy sont des éléments intrinsèques . Object.clone et System.arraycopy utilisent generate_unchecked_arraycopy . Et si nous allons plus loin, nous pourrions voir qu'après que HotSpot sélectionne une implémentation concrète, dépendant du système d'exploitation, etc.

Longly. Voyons le code de Hotspot . Tout d’abord, nous verrons que Object.clone (LibraryCallKit :: inline_native_clone) utilise generate_arraycopy, qui était utilisé pour System.arraycopy dans le cas de -XX : -ReduceInitialCardMarks. Sinon, LibraryCallKit :: copy_to_clone, qui initialise un nouveau tableau dans la mémoire RAW (si -XX: + RéduireBulkZeroing, qui activait par défaut) . En revanche, System.arraycopy utilise generate_arraycopy, essayez de vérifier ReduceBulkZeroing (et bien d’autres cas). ) et éliminer la mise à zéro des tableaux aussi, avec les contrôles supplémentaires mentionnés et il ferait également des contrôles supplémentaires pour s’assurer que tous les éléments sont initialisés, contrairement à Object.clone. Enfin, dans le meilleur des cas, les deux utilisent generate_unchecked_arraycopy.

Ci-dessous, je montre quelques points de repère pour voir cet effet sur la pratique:

  1. Le premier est simplement un repère, la seule différence par rapport à la réponse précédente, à savoir que les tableaux ne sont pas triés; Nous voyons que arraycopy est plus lent (mais pas deux fois), résultats - https://Pastebin.com/ny56Ag1z ;
  2. Deuxièmement, j'ajoute l'option -XX: -ReduceBulkZeroing et je constate maintenant que les performances des deux méthodes sont très proches. Résultats - https://Pastebin.com/ZDAeQWwx ;
  3. Je suppose également que nous aurons la différence Old/Young, en raison de l'alignement des tableaux (c'est une fonctionnalité de Java GC, lorsque nous appelons GC, l'alignement des tableaux est modifié, il est facile de l'observer avec JOL ). J'ai été surpris de constater que les performances deviennent généralement les mêmes et se dégradent pour les deux méthodes. Résultats - https://Pastebin.com/bTt5SJ8r . Pour ceux qui croient en des nombres concrets, le débit de System.arraycopy est supérieur à Object.clone.

Premier repère:

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;
    }

}

En exécutant ce test sur mon PC, j’ai eu ceci - https://Pastebin.com/ny56Ag1z . La différence n’est pas si grande, mais elle existe toujours.

Le deuxième point de référence, je n’ajoute qu’un paramètre -XX: -ReduceBulkZeroing et j’ai obtenu les résultats https://Pastebin.com/ZDAeQWwx . Non, nous voyons que pour Young Gen, la différence est aussi beaucoup moins.

Dans le troisième point de référence, j'ai modifié uniquement la méthode de configuration et réactivé l'option de réduire le nombre: 2:

@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();
    }
}

La différence est beaucoup moins (peut-être dans l'intervalle d'erreur) - https://Pastebin.com/bTt5SJ8r .

Avertissement

C'est aussi peut être faux. Vous devriez vérifier par vous-même.

En outre

Je pense qu’il est intéressant de regarder le processus des repères:

# 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

Pour 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

Nous pouvons observer une baisse de performance ici pour System.arraycopy. J'ai vu une image similaire pour Streams et il y avait un bogue dans les compilateurs ..__ Je suppose que cela pourrait aussi être un bogue dans les compilateurs. Quoi qu’il en soit, il est étrange qu’après 3 échauffements réduisent les performances.

METTRE &AGRAVE; JOUR

Qu'en est-il de la vérification de type

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;
    }

}

La différence n'est pas observée - https://Pastebin.com/ufxCZVaC . Je suppose qu'une explication est simple, car System.arraycopy est intrinsèque à chaud dans ce cas, l'implémentation réelle serait simplement intégrée, sans typage. , etc.

Remarque

J'ai convenu avec Radiodef que vous pourriez trouver intéressant de lire billet de blog , l'auteur de ce blog est le créateur (ou l'un des créateurs) de JMH .

3
egorlitvinenko

La différence de performances provient du fait que l’on passe à l’étape où le tableau est mis à zéro.

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();
}

Cependant, dans les cas où la performance de la copie d'un tableau fait une différence significative, il est généralement préférable d'utiliser la double mise en mémoire tampon.

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);
0
Paul Smith