.toArray (nouveau MyClass [0]) ou .toArray (nouveau MyClass [myList.size ()])?

176

En supposant que j'ai une ArrayList

ArrayList<MyClass> myList;

Et je veux appeler toArray, y a-t-il une raison de performance à utiliser

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

plus de

MyClass[] arr = myList.toArray(new MyClass[0]);

?

Je préfère le deuxième style, car il est moins verbeux, et j'ai supposé que le compilateur s'assurait que le tableau vide ne soit pas vraiment créé, mais je me suis demandé si c'était vrai.

Bien sûr, dans 99% des cas, cela ne fait aucune différence dans un sens ou dans l'autre, mais j'aimerais garder un style cohérent entre mon code normal et mes boucles internes optimisées ...

Itsadok
la source
7
On dirait que la question a maintenant été réglée dans un nouveau billet de blog d'Aleksey Shipilёv, Arrays of Wisdom of the Ancients !
glts
7
Extrait du billet de blog: "En bout de ligne: toArray (nouveau T [0]) semble plus rapide, plus sûr et plus propre sur le plan contractuel, et devrait donc être le choix par défaut maintenant."
DavidS

Réponses:

109

Contre-intuitivement, la version la plus rapide, sur Hotspot 8, est:

MyClass[] arr = myList.toArray(new MyClass[0]);

J'ai exécuté un micro-benchmark en utilisant jmh, les résultats et le code sont ci-dessous, montrant que la version avec un tableau vide surpasse systématiquement la version avec un tableau prédéfini. Notez que si vous pouvez réutiliser un tableau existant de la taille correcte, le résultat peut être différent.

Résultats de référence (score en microsecondes, plus petit = meilleur):

Benchmark                      (n)  Mode  Samples    Score   Error  Units
c.a.p.SO29378922.preSize         1  avgt       30    0.025  0.001  us/op
c.a.p.SO29378922.preSize       100  avgt       30    0.155  0.004  us/op
c.a.p.SO29378922.preSize      1000  avgt       30    1.512  0.031  us/op
c.a.p.SO29378922.preSize      5000  avgt       30    6.884  0.130  us/op
c.a.p.SO29378922.preSize     10000  avgt       30   13.147  0.199  us/op
c.a.p.SO29378922.preSize    100000  avgt       30  159.977  5.292  us/op
c.a.p.SO29378922.resize          1  avgt       30    0.019  0.000  us/op
c.a.p.SO29378922.resize        100  avgt       30    0.133  0.003  us/op
c.a.p.SO29378922.resize       1000  avgt       30    1.075  0.022  us/op
c.a.p.SO29378922.resize       5000  avgt       30    5.318  0.121  us/op
c.a.p.SO29378922.resize      10000  avgt       30   10.652  0.227  us/op
c.a.p.SO29378922.resize     100000  avgt       30  139.692  8.957  us/op

Pour référence, le code:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class SO29378922 {
  @Param({"1", "100", "1000", "5000", "10000", "100000"}) int n;
  private final List<Integer> list = new ArrayList<>();
  @Setup public void populateList() {
    for (int i = 0; i < n; i++) list.add(0);
  }
  @Benchmark public Integer[] preSize() {
    return list.toArray(new Integer[n]);
  }
  @Benchmark public Integer[] resize() {
    return list.toArray(new Integer[0]);
  }
}

Vous pouvez trouver des résultats similaires, une analyse complète et une discussion dans le billet de blog Arrays of Wisdom of the Ancients . Pour résumer: le compilateur JVM et JIT contient plusieurs optimisations qui lui permettent de créer et d'initialiser à moindre coût un nouveau tableau correctement dimensionné, et ces optimisations ne peuvent pas être utilisées si vous créez le tableau vous-même.

assylies
la source
2
Commentaire très intéressant. Je suis surpris que personne n'ait fait de commentaires à ce sujet. Je suppose que c'est parce que cela contredit les autres réponses ici, en ce qui concerne la vitesse. Il est également intéressant de noter que la réputation de ces gars est presque supérieure à toutes les autres réponses combinées.
Pimp Trizkit
Je digresse. J'aimerais aussi voir des repères pour MyClass[] arr = myList.stream().toArray(MyClass[]::new);.. ce qui, je suppose, serait plus lent. Aussi, je voudrais voir des points de repère pour la différence avec la déclaration de tableau. Comme dans la différence entre: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);et MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... ou ne devrait-il pas y avoir de différence? Je suppose que ces deux problèmes sont en dehors des toArrayévénements fonctionnels. Mais salut! Je ne pensais pas que j'apprendrais les autres différences complexes.
Pimp Trizkit
1
@PimpTrizkit Vient de vérifier: l'utilisation d'une variable supplémentaire ne fait aucune différence comme prévu, l'utilisation d'un flux prend entre 60% et 100% de temps en plus pour appeler toArraydirectement (plus la taille est petite, plus la surcharge relative est grande)
assylias
Wow, c'était une réponse rapide! Merci! Oui, je m'en doutais. La conversion en flux ne semblait pas efficace. Mais tu ne sais jamais!
Pimp Trizkit
2
Cette même conclusion a été trouvée ici: shipilev.net/blog/2016/arrays-wisdom-ancients
user167019
122

À partir de ArrayList dans Java 5 , le tableau sera déjà rempli s'il a la bonne taille (ou est plus grand). par conséquent

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

va créer un objet tableau, le remplir et le renvoyer à "arr". D'autre part

MyClass[] arr = myList.toArray(new MyClass[0]);

créera deux tableaux. Le second est un tableau de MyClass de longueur 0. Il y a donc une création d'objet pour un objet qui sera immédiatement jeté. Pour autant que le code source le suggère, le compilateur / JIT ne peut pas optimiser celui-ci pour qu'il ne soit pas créé. En outre, l'utilisation de l'objet de longueur nulle entraîne des transtypages dans la méthode toArray ().

Voir la source de ArrayList.toArray ():

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Utilisez la première méthode pour qu'un seul objet soit créé et évitez les castings (implicites mais néanmoins coûteux).

Georgi
la source
1
Deux commentaires pourraient intéresser quelqu'un: 1) LinkedList.toArray (T [] a) est encore plus lent (utilise la réflexion: Array.newInstance) et plus complexe; 2) D'un autre côté, dans la version JDK7, j'ai été très surpris de découvrir que Array.newInstance, généralement extrêmement lent, fonctionne presque aussi vite que la création de tableaux habituelle!
java.is.for.desktop
1
@ktaria size est un membre privé de ArrayList, spécifiant **** surprise **** la taille. Voir ArrayList SourceCode
MyPasswordIsLasercats
3
Estimer les performances sans benchmarks ne fonctionne que dans des cas triviaux. En fait, new Myclass[0]c'est plus rapide: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S
Ce n'est plus une réponse valide à partir de JDK6 +
Антон Антонов
28

À partir de l'inspection JetBrains Intellij Idea:

Il existe deux styles pour convertir une collection en tableau: soit en utilisant un tableau pré-dimensionné (comme c.toArray (new String [c.size ()]) ) ou en utilisant un tableau vide (comme c.toArray (new String [ 0]) .

Dans les anciennes versions de Java, l'utilisation d'un tableau prédimensionné était recommandée, car l'appel de réflexion nécessaire pour créer un tableau de taille appropriée était assez lent. Cependant, depuis les dernières mises à jour d'OpenJDK 6, cet appel était intrinsèque, rendant les performances de la version de tableau vide les mêmes et parfois même meilleures, par rapport à la version pré-dimensionnée. Le passage d'un tableau pré-dimensionné est également dangereux pour une collection simultanée ou synchronisée car une course aux données est possible entre la taille et l' appel toArray, ce qui peut entraîner des valeurs nulles supplémentaires à la fin du tableau, si la collection a été réduite simultanément pendant l'opération.

Cette inspection permet de suivre le style uniforme: soit en utilisant un tableau vide (ce qui est recommandé dans Java moderne), soit en utilisant un tableau pré-dimensionné (qui pourrait être plus rapide dans les anciennes versions de Java ou les JVM non basées sur HotSpot).

Антон Антонов
la source
Si tout cela est du texte copié / cité, pourrions-nous le formater en conséquence et également fournir un lien vers la source? Je suis en fait venu ici à cause de l'inspection IntelliJ et je suis très intéressé par le lien pour consulter toutes leurs inspections et le raisonnement qui les sous-tend.
Tim Büthe
3
Ici vous pouvez consulter les textes des inspections: github.com/JetBrains/intellij-community/tree/master/plugins/…
Антон Антонов
17

Les JVM modernes optimisent la construction de la matrice réfléchissante dans ce cas, donc la différence de performances est minime. Nommer la collection deux fois dans un tel code standard n'est pas une bonne idée, alors j'éviterais la première méthode. Un autre avantage du second est qu'il fonctionne avec des collections synchronisées et simultanées. Si vous souhaitez faire une optimisation, réutilisez le tableau vide (les tableaux vides sont immuables et peuvent être partagés), ou utilisez un profileur (!).

Tom Hawtin - ligne de pêche
la source
2
Le vote pour «réutiliser le tableau vide», car c'est un compromis entre la lisibilité et les performances potentielles qui mérite d'être pris en considération. En passant un argument déclaré private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]n'empêche pas le tableau retourné d'être construit par la réflexion, mais il n'empêcher un tableau supplémentaire construit chaque à chaque fois.
Michael Scheper
Machael a raison, si vous utilisez un tableau de longueur nulle, il n'y a aucun moyen de contourner: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); ce qui serait superflu si la taille était> = actualSize (JDK7)
Alex
Si vous pouvez citer "les machines virtuelles Java modernes optimisent la construction de réseaux réfléchissants dans ce cas", je serai ravi de voter pour cette réponse.
Tom Panning
J'apprends ici. Si à la place j'utilise: MyClass[] arr = myList.stream().toArray(MyClass[]::new);cela aiderait ou ferait-il mal avec les collections synchronisées et simultanées. Et pourquoi? S'il vous plaît.
Pimp Trizkit
3

toArray vérifie que le tableau passé est de la bonne taille (c'est-à-dire suffisamment grand pour contenir les éléments de votre liste) et si c'est le cas, l'utilise. Par conséquent, si la taille du tableau le fournissait plus petite que nécessaire, un nouveau tableau sera créé par réflexe.

Dans votre cas, un tableau de taille zéro est immuable et peut donc être élevé en toute sécurité à une variable finale statique, ce qui pourrait rendre votre code un peu plus propre, ce qui évite de créer le tableau à chaque appel. Un nouveau tableau sera de toute façon créé à l'intérieur de la méthode, c'est donc une optimisation de la lisibilité.

La version la plus rapide consiste sans doute à transmettre le tableau d'une taille correcte, mais à moins que vous ne puissiez prouver que ce code est un goulot d'étranglement des performances, préférez la lisibilité aux performances d'exécution jusqu'à preuve du contraire.

Dave Cheney
la source
2

Le premier cas est plus efficace.

En effet, dans le second cas:

MyClass[] arr = myList.toArray(new MyClass[0]);

le runtime crée en fait un tableau vide (avec une taille nulle), puis à l'intérieur de la méthode toArray crée un autre tableau pour s'adapter aux données réelles. Cette création se fait en utilisant la réflexion en utilisant le code suivant (extrait de jdk1.5.0_10):

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.
    newInstance(a.getClass().getComponentType(), size);
System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

En utilisant le premier formulaire, vous évitez la création d'un deuxième tableau et évitez également le code de réflexion.

Panagiotis Korros
la source
toArray () n'utilise pas la réflexion. Du moins tant que vous ne comptez pas "lancer" à la réflexion, de toute façon ;-).
Georgi
toArray (T []) le fait. Il doit créer un tableau du type approprié. Les JVM modernes optimisent ce type de réflexion pour qu'il soit à peu près à la même vitesse que la version non réfléchissante.
Tom Hawtin - tackline
Je pense qu'il utilise la réflexion. Le JDK 1.5.0_10 le fait à coup sûr et la réflexion est le seul moyen que je connaisse pour créer un tableau d'un type que vous ne connaissez pas au moment de la compilation.
Panagiotis Korros
Ensuite, l'un des exemples de code source (celui ci-dessus ou le mien) est obsolète. Malheureusement, je n'ai pas trouvé de numéro de sous-version correct pour le mien.
Georgi
1
Georgi, votre code provient du JDK 1.6 et si vous voyez l'implémentation de la méthode Arrays.copyTo, vous verrez que l'implémentation utilise la réflexion.
Panagiotis Korros
-1

Le second est légèrement plus lisible, mais il y a si peu d'amélioration que cela n'en vaut pas la peine. La première méthode est plus rapide, sans inconvénient à l'exécution, c'est donc ce que j'utilise. Mais je l'écris de la deuxième façon, car c'est plus rapide à taper. Ensuite, mon IDE le signale comme un avertissement et propose de le réparer. D'une simple pression sur une touche, il convertit le code du deuxième type au premier.

MiguelMunoz
la source
-2

L'utilisation de 'toArray' avec le tableau de la taille correcte fonctionnera mieux car l'alternative créera d'abord le tableau de taille zéro, puis le tableau de la taille correcte. Cependant, comme vous le dites, la différence sera probablement négligeable.

Notez également que le compilateur javac n'effectue aucune optimisation. De nos jours, toutes les optimisations sont effectuées par les compilateurs JIT / HotSpot lors de l'exécution. Je n'ai connaissance d'aucune optimisation autour de «toArray» dans les machines virtuelles Java.

La réponse à votre question est donc en grande partie une question de style, mais par souci de cohérence, elle doit faire partie de toutes les normes de codage auxquelles vous adhérez (qu'elles soient documentées ou non).

Matthieu Murdoch
la source
OTOH, si la norme consiste à utiliser un tableau de longueur nulle, les cas qui s'écartent impliquent que les performances sont un problème.
Michael Scheper
-5

exemple de code pour entier:

Integer[] arr = myList.toArray(new integer[0]);
Rasol
la source