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 La différence est expliquée ici , mais cela ne répond pas à la question de savoir pourquoi Josh Bloch considère System.arraycopy
?clone()
comme le moyen le plus rapide.
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()
).
D'une part, clone()
n'a pas à faire la vérification typographique que System.arraycopy()
fait.
Je veux corriger et compléter les réponses précédentes.
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:
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 .
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);