Vaut-il mieux utiliser System.arraycopy (…) qu'une boucle for pour copier des tableaux?

89

Je veux créer un nouveau tableau d'objets rassemblant deux tableaux plus petits.

Ils ne peuvent pas être nuls, mais la taille peut être égale à 0.

Je ne peux pas choisir entre ces deux méthodes: sont-elles équivalentes ou l'une est-elle plus efficace (par exemple, system.arraycopy () copie des morceaux entiers)?

MyObject[] things = new MyObject[publicThings.length+privateThings.length];
System.arraycopy(publicThings, 0, things, 0, publicThings.length);
System.arraycopy(privateThings, 0, things,  publicThings.length, privateThings.length);

ou

MyObject[] things = new MyObject[publicThings.length+privateThings.length];
for (int i = 0; i < things.length; i++) {
    if (i<publicThings.length){
        things[i] = publicThings[i]
    } else {
        things[i] = privateThings[i-publicThings.length]        
    }
}

La seule différence est-elle l'aspect du code?

EDIT: merci pour la question liée, mais ils semblent avoir une discussion non résolue:

Est-ce vraiment plus rapide si it is not for native types: byte [], Object [], char []? dans tous les autres cas, une vérification de type est exécutée, ce qui serait mon cas et donc serait équivalent ... non?

Sur une autre question liée, ils disent que the size matters a lot, pour une taille> 24 system.arraycopy () gagne, pour moins de 10, le manuel pour la boucle est meilleur ...

Maintenant, je suis vraiment confus.

Daren
la source
16
arraycopy()est un appel natif, qui est certainement plus rapide.
Sotirios Delimanolis
4
Avez-vous essayé de comparer les deux implémentations différentes?
Alex
5
Jetez un oeil ici: stackoverflow.com/questions/2772152/…
Sotirios Delimanolis
15
Vous devez choisir celui que vous trouvez le plus lisible et le plus facile à entretenir à l'avenir. Ce n'est que lorsque vous avez déterminé que c'est la source d'un goulot d'étranglement que vous devriez changer votre approche.
arshajii
1
Ne réinventez pas la roue!
camickr

Réponses:

87
public void testHardCopyBytes()
{
    byte[] bytes = new byte[0x5000000]; /*~83mb buffer*/
    byte[] out = new byte[bytes.length];
    for(int i = 0; i < out.length; i++)
    {
        out[i] = bytes[i];
    }
}

public void testArrayCopyBytes()
{
    byte[] bytes = new byte[0x5000000]; /*~83mb buffer*/
    byte[] out = new byte[bytes.length];
    System.arraycopy(bytes, 0, out, 0, out.length);
}

Je sais que les tests JUnit ne sont pas vraiment les meilleurs pour l'analyse comparative, mais
testHardCopyBytes a pris 0,157 s pour terminer
et
testArrayCopyBytes a pris 0,086 s pour terminer.

Je pense que cela dépend de la machine virtuelle, mais il semble que cela copie des blocs de mémoire au lieu de copier des éléments de tableau unique. Cela augmenterait absolument les performances.

EDIT:
Il semble que les performances de System.arraycopy soient partout. Lorsque des chaînes sont utilisées au lieu d'octets et que les tableaux sont petits (taille 10), j'obtiens ces résultats:

    String HC:  60306 ns
    String AC:  4812 ns
    byte HC:    4490 ns
    byte AC:    9945 ns

Voici à quoi cela ressemble lorsque les tableaux sont à la taille 0x1000000. Il semble que System.arraycopy gagne définitivement avec des tableaux plus grands.

    Strs HC:  51730575 ns
    Strs AC:  24033154 ns
    Bytes HC: 28521827 ns
    Bytes AC: 5264961 ns

Comme c'est étrange!

Merci Daren d'avoir signalé que les références se copient différemment. Cela en a fait un problème beaucoup plus intéressant!

Trent petit
la source
2
Merci pour vos efforts, mais vous avez manqué des points apparemment cruciaux: les types non natifs (créez une classe aléatoire avec anythign pour que le tableau contienne des références) et la taille ... semble pour une taille de tableau plus petite, la boucle for manuelle est plus rapide. Voulez-vous corriger cela?
Daren le
2
Oh wow, tu as raison! C'est intéressant. Placer des chaînes dans ces tableaux au lieu d'octets fait une énorme différence: <<< testHardCopyStrs: 0.161s >>> <<< testArrayCopyStrs: 0.170s >>>
Trent Small
Quels résultats obtenez-vous alors? aussi intéressant serait d'essayer avec une taille de tableau = 10 ... merci! (J'aurais aimé avoir mon IDE ici, je coder sans compilateur).
Daren le
Eh bien maintenant, je les ai enveloppés dans certains appels System.nanoTime () et j'ai défini size = 10 pour voir combien de nanosecondes chacun prend. On dirait que pour les petits tableaux primitifs, les boucles sont meilleures; pour les références, arrayCopy est meilleur .: <<< testHardCopyBytes: 4491 ns >>> <<< testHardCopyStrs: 56778 ns >>> <<< testArrayCopyBytes: 10265 ns >>> <<< testArrayCopyStrs: 4490 ns >>>
Trent Petit
Des résultats très intéressants! Merci beaucoup! pouvez-vous s'il vous plaît modifier votre réponse pour l'inclure, et je l'accepterai avec plaisir afin qu'elle reste la première à voir ... vous avez déjà obtenu mon vote. :)
Daren
36

Arrays.copyOf(T[], int)est plus facile à lire. En interne, il utilise System.arraycopy()ce qui est un appel natif.

Vous ne pouvez pas l'obtenir plus rapidement!

Philipp Sander
la source
il semble que vous pouvez dépendre de pas mal de choses, mais merci d'avoir signalé cette fonction que je ne connaissais pas et qui est en effet plus facile à lire. :)
Daren
Oui! cela dépend de pas mal de choses comme l'a dit @Svetoslav Tsolov. Je voulais juste souligner Arrays.copyOf
Philipp Sander
2
copyOfne peut pas toujours remplacer arraycopy, mais c'est approprié pour ce cas d'utilisation.
Blaisorblade
1
NB Si vous regardez les performances, cela ne sera pas aussi rapide que System.arraycopy () car il nécessite une allocation de mémoire. S'il s'agit d'une boucle, des allocations répétées conduiront à un garbage collection qui sera un énorme impact sur les performances.
Will Calderwood
1
@PhilippSander Juste pour vérifier que je n'étais pas stupide, j'ai ajouté du code pour copier un tableau de 1 Mo dans ma boucle de jeu, qui ne déclenche presque jamais GC. Avec Array.copyOf (), mon DVM appelait GC 5 fois par seconde et le jeu devenait extrêmement lent. Je pense qu'il est prudent de dire qu'il y a une allocation de mémoire.
Will Calderwood
17

Cela dépend de la machine virtuelle, mais System.arraycopy devrait vous fournir le plus proche possible des performances natives.

J'ai travaillé pendant 2 ans en tant que développeur java pour les systèmes embarqués (où les performances sont une priorité absolue) et partout où System.arraycopy pouvait être utilisé, je l'ai principalement utilisé / vu utilisé dans du code existant. Il est toujours préférable aux boucles lorsque les performances sont un problème. Si les performances ne sont pas un gros problème, je suivrais la boucle. Beaucoup plus facile à lire.


la source
Des éléments tels que la taille et le type de tableau (basique ou hérité) semblent affecter les performances.
Daren le
2
Oui, ce n'est pas une «performance native» en soi, c'est pourquoi j'ai dit que je l'utilisais «principalement» là où je le pouvais (vous remarquerez qu'elle l'emporte principalement sur la copie en boucle). Je suppose que la raison est la suivante: lorsqu'il s'agit d'un petit tableau de type primitif, le «coût de l'appel» est supérieur à l'augmentation des performances. L'utilisation de JNI peut dégrader les performances pour la même raison - le code natif lui-même est rapide, mais l'appel à partir d'un processus java - pas tellement.
Correction mineure, Arrays.copy n'est pas JNI, c'est un intrinsèque. Les intrinsèques sont beaucoup plus rapides que JNI. Comment et quand le compilateur JIT le transforme en un intrinsèque dépend de la JVM / compilateur utilisé.
Nitsan Wakart
1
Arrays.copyn'existe pas, Arrays.copyOfest une fonction de bibliothèque.
Blaisorblade
12

Au lieu de me fier à des spéculations et peut-être à des informations obsolètes, j'ai exécuté des tests de . En fait, Caliper est livré avec quelques exemples, dont un CopyArrayBenchmarkqui mesure exactement cette question! Tout ce que tu as à faire c'est courir

mvn exec:java -Dexec.mainClass=com.google.caliper.runner.CaliperMain -Dexec.args=examples.CopyArrayBenchmark

Mes résultats sont basés sur la VM serveur 64 bits Java HotSpot (TM) d'Oracle, 1.8.0_31-b13, fonctionnant sur un MacBook Pro mi-2010 (macOS 10.11.6 avec Intel Arrandale i7, 8 Gio de RAM). Je ne pense pas qu'il soit utile de publier les données de synchronisation brutes. Je vais plutôt résumer les conclusions avec les visualisations à l'appui.

En résumé:

  • Écrire une forboucle manuelle pour copier chaque élément dans un tableau nouvellement instancié n'est jamais avantageux, que ce soit pour des tableaux courts ou des tableaux longs.
  • Arrays.copyOf(array, array.length)et array.clone()sont tous les deux toujours rapides. Ces deux techniques sont presque identiques en performance; celui que vous choisissez est une question de goût.
  • System.arraycopy(src, 0, dest, 0, src.length)est presque aussi rapide que et , mais pas tout à fait systématiquement. (Voir le cas pour 50000 s.) À cause de cela, et de la verbosité de l'appel, je recommanderais si vous avez besoin d'un contrôle précis sur quels éléments sont copiés et où.Arrays.copyOf(array, array.length)array.clone()intSystem.arraycopy()

Voici les graphiques de synchronisation:

Délais de copie de tableaux de longueur 5 Délais de copie de tableaux de longueur 500 Délais de copie de tableaux de longueur 50000

200_succès
la source
3
Y a-t-il quelque chose de bizarre dans la copie int? Il semble étrange que la copie en matrice soit lente sur les ints à grande échelle.
whaleberg
6

L'exécution de méthodes natives comme Arrays.copyOf(T[], int)cela a une surcharge, mais cela ne signifie pas que ce n'est pas rapide car vous l'exécutez en utilisant JNI.

Le moyen le plus simple est d'écrire un benchmark et un test.

Vous pouvez vérifier que Arrays.copyOf(T[], int)c'est plus rapide que votre forboucle normale .

Le code de référence d' ici : -

public void test(int copySize, int copyCount, int testRep) {
    System.out.println("Copy size = " + copySize);
    System.out.println("Copy count = " + copyCount);
    System.out.println();
    for (int i = testRep; i > 0; --i) {
        copy(copySize, copyCount);
        loop(copySize, copyCount);
    }
    System.out.println();
}

public void copy(int copySize, int copyCount) {
    int[] src = newSrc(copySize + 1);
    int[] dst = new int[copySize + 1];
    long begin = System.nanoTime();
    for (int count = copyCount; count > 0; --count) {
        System.arraycopy(src, 1, dst, 0, copySize);
        dst[copySize] = src[copySize] + 1;
        System.arraycopy(dst, 0, src, 0, copySize);
        src[copySize] = dst[copySize];
    }
    long end = System.nanoTime();
    System.out.println("Arraycopy: " + (end - begin) / 1e9 + " s");
}

public void loop(int copySize, int copyCount) {
    int[] src = newSrc(copySize + 1);
    int[] dst = new int[copySize + 1];
    long begin = System.nanoTime();
    for (int count = copyCount; count > 0; --count) {
        for (int i = copySize - 1; i >= 0; --i) {
            dst[i] = src[i + 1];
        }
        dst[copySize] = src[copySize] + 1;
        for (int i = copySize - 1; i >= 0; --i) {
            src[i] = dst[i];
        }
        src[copySize] = dst[copySize];
    }
    long end = System.nanoTime();
    System.out.println("Man. loop: " + (end - begin) / 1e9 + " s");
}

public int[] newSrc(int arraySize) {
    int[] src = new int[arraySize];
    for (int i = arraySize - 1; i >= 0; --i) {
        src[i] = i;
    }
    return src;
}

System.arraycopy()utilise JNI (Java Native Interface) pour copier un tableau (ou des parties de celui-ci), il est donc extrêmement rapide, comme vous pouvez le confirmer ici

Rahul Tripathi
la source
Ce code utilise int [] pouvez-vous l'essayer avec String [] (initialisé avec des valeurs différentes: "1", "2", etc. car ils sont immuables
Daren
1
JNI est très lent . System.arraycopyne l'utilise pas.
Chai T. Rex
Non, System.arraycopyn'utilise pas JNI, qui sert uniquement à appeler des bibliothèques tierces. Au lieu de cela, il s'agit d'un appel natif, ce qui signifie qu'il existe une implémentation native dans la machine virtuelle.
spheenik le
6

Il n'est pas possible que ce Arrays.copyOfsoit plus rapide que System.arraycopypuisqu'il s'agit de la mise en œuvre de copyOf:

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;
}
DimaD
la source
4

System.arraycopy()est un appel natif qui effectue une opération de copie directement en mémoire. Une copie de mémoire unique serait toujours plus rapide que votre boucle for

Nandkumar Tekale
la source
3
J'ai lu que pour les types non natifs (toute classe créée, comme la mienne), cela peut ne pas être aussi efficace ... et pour les petites tailles (mon cas), le manuel pour la boucle peut être mieux ...
Daren le
En effet, System.arraycopy () a une surcharge donc pour les petits tableaux (n = ~ 10) une boucle est en fait plus rapide
RecursiveExceptionException