Question Est-il préférable d'appeler ToList () ou ToArray () dans les requêtes LINQ?


Je rencontre souvent le cas où je veux évaluer une requête là où je la déclare. C'est généralement parce que j'ai besoin de parcourir it plusieurs fois et c'est coûteux à calculer. Par exemple:

string raw = "...";
var lines = (from l in raw.Split('\n')
             let ll = l.Trim()
             where !string.IsNullOrEmpty(ll)
             select ll).ToList();

Cela fonctionne bien. Mais si je ne vais pas modifier le résultat, alors je pourrais aussi bien appeler ToArray() au lieu de ToList().

Je me demande cependant si ToArray () est implémenté en premier appelant ToList () et est donc moins efficace en mémoire que l'appel de ToList ().

Suis-je fou? Devrais-je simplement appeler ToArray() - sûr et sécurisé dans la connaissance que la mémoire ne sera pas attribuée deux fois?


428
2017-07-09 19:28


origine


Réponses:


Sauf si vous avez simplement besoin d'un tableau pour répondre à d'autres contraintes, vous devez utiliser ToList. Dans la majorité des scénarios ToArray va allouer plus de mémoire que ToList.

Les deux utilisent des tableaux pour le stockage, mais ToList a une contrainte plus souple. Il a besoin que le tableau soit au moins aussi grand que le nombre d'éléments dans la collection. Si le tableau est plus grand, ce n'est pas un problème. toutefois ToArray a besoin que le tableau soit dimensionné exactement au nombre d'éléments.

Pour répondre à cette contrainte ToArray fait souvent une allocation de plus que ToList. Une fois qu'il a un tableau assez grand, il alloue un tableau qui est exactement la taille correcte et copie les éléments dans ce tableau. Le seul moment où il peut éviter cela est lorsque l'algorithme de croissance de la matrice coïncide avec le nombre d'éléments devant être stockés (certainement en minorité).

MODIFIER

Un couple de personnes m'a demandé à propos de la conséquence d'avoir la mémoire supplémentaire inutilisée dans le List<T> valeur.

C'est une préoccupation valable. Si la collection créée a une longue durée de vie, n'est jamais modifiée après avoir été créée et a de fortes chances d'atterrir dans le tas Gen2, alors vous feriez mieux de prendre l'allocation supplémentaire de ToArray à l'avant.

En général, je trouve que c'est le cas le plus rare. Il est beaucoup plus commun de voir beaucoup de ToArray appels qui sont immédiatement transmis à d'autres utilisations de la mémoire de courte durée, auquel cas ToList est manifestement meilleur.

La clé ici est de profiler, profiler et ensuite en profiler d'autres.


262
2018-05-01 17:42



La différence de performance sera insignifiante, puisque List<T> est implémenté comme un tableau de taille dynamique. Appeler soit ToArray() (qui utilise un interne Buffer<T> classe pour développer le tableau) ou ToList() (qui appelle le List<T>(IEnumerable<T>) constructeur) finira par être une question de les mettre dans un tableau et de faire croître le tableau jusqu'à ce qu'il les adapte tous.

Si vous désirez une confirmation concrète de ce fait, vérifiez l'implémentation des méthodes en question dans Reflector - vous verrez qu'elles se résument à un code presque identique.


154
2017-07-09 19:33



(sept ans plus tard ...)

Quelques autres (bonnes) réponses se sont concentrées sur les différences de performances microscopiques.

Ce post est juste un complément pour mentionner le différence sémantique qui existe entre le IEnumerator<T> produit par un tableau (T[]) par rapport à celui renvoyé par un List<T>.

Mieux illustré par exemple:

IList<int> source = Enumerable.Range(1, 10).ToArray();  // try changing to .ToList()

foreach (var x in source)
{
  if (x == 5)
    source[8] *= 100;
  Console.WriteLine(x);
}

Le code ci-dessus s'exécutera sans exception et produira la sortie:

1
2
3
4
5
6
7
8
900
dix

Cela montre que le IEnumarator<int> retourné par un int[] ne permet pas de savoir si le tableau a été modifié depuis la création de l'énumérateur.

Notez que j'ai déclaré la variable locale source en tant que IList<int>. De cette façon, je m'assure que le compilateur C # n’optimise pas le foreach déclaration dans quelque chose qui équivaut à un for (var idx = 0; idx < source.Length; idx++) { /* ... */ } boucle. C'est quelque chose que le compilateur C # pourrait faire si j'utilise var source = ...; au lieu. Dans ma version actuelle du framework .NET, l’énumérateur utilisé ici est un type de référence non public. System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32] mais bien sûr, ceci est un détail de mise en œuvre.

Maintenant, si je change .ToArray() dans .ToList(), Je reçois seulement:

1
2
3
4
5

suivi d'un System.InvalidOperationException exploser en disant:

La collection a été modifiée. l'opération d'énumération peut ne pas s'exécuter.

L'énumérateur sous-jacent dans ce cas est le type de valeur mutable publique System.Collections.Generic.List`1+Enumerator[System.Int32] (encadré dans un IEnumerator<int> boîte dans ce cas parce que j'utilise IList<int>).

En conclusion, l'énumérateur produit par un List<T> garde la trace de la modification de la liste pendant l'énumération, tandis que l'énumérateur produit par T[] ne fait pas. Alors considérez cette différence lorsque vous choisissez entre .ToList() et .ToArray().

Les gens en ajoutent souvent un supplémentaire  .ToArray() ou .ToList() contourner une collection qui vérifie si elle a été modifiée pendant la durée de vie d'un enquêteur.

(Si quelqu'un veut savoir Comment la List<> garde une trace pour savoir si la collection a été modifiée, il y a un domaine privé _version dans cette classe qui est changée à chaque fois que le List<> Est mis à jour.)


29
2017-12-20 16:03



Je suis d'accord avec @mquander que la différence de performance devrait être négligeable. Cependant, je voulais l'évaluer pour être sûr, alors je l'ai fait - et c'est insignifiant.

Testing with List<T> source:
ToArray time: 1934 ms (0.01934 ms/call), memory used: 4021 bytes/array
ToList  time: 1902 ms (0.01902 ms/call), memory used: 4045 bytes/List

Testing with array source:
ToArray time: 1957 ms (0.01957 ms/call), memory used: 4021 bytes/array
ToList  time: 2022 ms (0.02022 ms/call), memory used: 4045 bytes/List

Chaque tableau source / liste contenait 1000 éléments. Vous pouvez donc voir que les différences de temps et de mémoire sont négligeables.

Ma conclusion: vous pouvez aussi bien utiliser Lister(), depuis un List<T> fournit plus de fonctionnalités qu'un tableau, à moins que quelques octets de mémoire ne comptent vraiment pour vous.


24
2018-01-11 23:10



La mémoire sera toujours allouée deux fois - ou quelque chose de proche. Comme vous ne pouvez pas redimensionner un tableau, les deux méthodes utiliseront un mécanisme quelconque pour rassembler les données dans une collection croissante. (Eh bien, la liste est une collection croissante en soi.)

La liste utilise un tableau comme stockage interne et double la capacité en cas de besoin. Cela signifie qu'en moyenne, les deux tiers des articles ont été réaffectés au moins une fois, la moitié d'entre eux réattribués au moins deux fois, la moitié au moins trois fois, et ainsi de suite. Cela signifie que chaque article a été réaffecté en moyenne 1,3 fois, ce qui n’est pas très élevé.

Souvenez-vous également que si vous collectez des chaînes, la collection elle-même ne contient que les références aux chaînes, les chaînes elles-mêmes ne sont pas réaffectées.


20
2017-07-09 19:57



ToList()est généralement préférable si vous l'utilisez sur IEnumerable<T> (de l'ORM, par exemple). Si la longueur de la séquence n'est pas connue au début, ToArray() crée une collection de longueur dynamique telle que List, puis la convertit en tableau, ce qui prend plus de temps.


20
2018-02-01 14:55



modifier: La dernière partie de cette réponse n'est pas valide. Cependant, le reste est toujours une information utile, donc je vais le laisser.

Je sais que c'est un vieux billet, mais après avoir posé la même question et fait quelques recherches, j'ai trouvé quelque chose d'intéressant qui pourrait valoir la peine d'être partagé.

Tout d'abord, je suis d'accord avec @mquander et sa réponse. Il a raison de dire que, sur le plan des performances, les deux sont identiques.

Cependant, j'ai utilisé Reflector pour examiner les méthodes de la System.Linq.Enumerable extensions namespace, et j'ai remarqué une optimisation très commune.
Autant que possible, le IEnumerable<T> la source est jeté à IList<T> ou ICollection<T> pour optimiser la méthode. Par exemple, regardez ElementAt(int).

Fait intéressant, Microsoft a choisi d’optimiser uniquement pour IList<T>, mais non IList. Il semble que Microsoft préfère utiliser le IList<T> interface.

System.Array seulement implémente IList, donc il ne bénéficiera d'aucune de ces optimisations d'extension.
Par conséquent, je considère que la meilleure pratique consiste à utiliser .ToList() méthode.
Si vous utilisez l'une des méthodes d'extension ou passez la liste à une autre méthode, il est possible qu'elle soit optimisée pour un IList<T>.


16
2017-07-12 19:55



Vous devez baser votre décision pour aller ToList ou ToArray basé sur ce que, idéalement, le choix de conception est. Si vous voulez une collection qui ne peut qu'être itérée et accessible par index, choisissez ToArray. Si vous voulez des fonctionnalités supplémentaires d'ajout et de suppression de la collection plus tard sans trop de tracas, faites un ToList (Pas vraiment que vous ne pouvez pas ajouter à un tableau, mais ce n'est pas le bon outil pour cela habituellement).

Si la performance est importante, vous devriez également considérer ce qui serait plus rapide à utiliser. De façon réaliste, vous n'appellerez pas ToList ou ToArray un million de fois, mais pourrait travailler sur la collection obtenue un million de fois. À cet égard [] c'est mieux, depuis List<> est [] avec quelques frais généraux. Voir ce fil pour une comparaison d'efficacité: Lequel est le plus efficace: Liste <int> ou int []

Dans mes propres tests il y a quelque temps, j'avais trouvé ToArray plus rapide. Et je ne suis pas sûr de la fausseté des tests. La différence de performance est si insignifiante, ce qui n'est visible que si vous exécutez ces requêtes dans une boucle des millions de fois.


13
2017-12-07 10:42



Une réponse très tardive mais je pense que ce sera utile pour les googleurs.

Ils aspirent tous les deux quand ils ont créé en utilisant linq. Ils implémentent tous les deux le même code pour redimensionner le tampon si nécessaire. ToArray utilise en interne une classe pour convertir IEnumerable<> au tableau, en allouant un tableau de 4 éléments. Si cela ne suffit pas, doublez la taille en créant un nouveau tableau, doublez la taille du courant et copiez-y le tableau courant. A la fin, il alloue un nouveau tableau de nombre de vos articles. Si votre requête renvoie 129 éléments, ToArray effectuera 6 attributions et des opérations de copie de mémoire pour créer un tableau à 256 éléments et un autre tableau de 129 à renvoyer. tant pour l'efficacité de la mémoire.

ToList fait la même chose, mais ignore la dernière allocation car vous pouvez ajouter des éléments dans le futur. La liste ne se soucie pas si elle est créée à partir d'une requête linq ou créée manuellement.

pour la création La liste est meilleure avec de la mémoire, mais pire avec cpu puisque la liste est une solution générique, chaque action nécessite des vérifications de portée supplémentaires aux contrôles internes de la plage .net pour les tableaux.

Ainsi, si vous parcourez trop souvent votre jeu de résultats, les tableaux sont corrects, car ils impliquent moins de contrôles de plages que de listes, et les compilateurs optimisent généralement les tableaux pour un accès séquentiel.

L'allocation d'initialisation de List peut être meilleure si vous spécifiez le paramètre de capacité lorsque vous le créez. Dans ce cas, le tableau ne sera alloué qu'une seule fois, en supposant que vous connaissiez la taille du résultat. ToList de linq ne spécifie pas de surcharge pour le fournir, nous devons donc créer notre méthode d’extension qui crée une liste avec une capacité donnée et utilise ensuite List<>.AddRange.

Pour finir cette réponse, je dois écrire les phrases suivantes

  1. À la fin, vous pouvez utiliser un ToArray ou ToList, les performances ne seront pas si différentes (voir la réponse de @EMP).
  2. Vous utilisez C #. Si vous avez besoin de performances, ne vous inquiétez pas d'écrire du code haute performance, mais craignez de ne pas écrire de mauvais code de performance.
  3. Toujours cibler x64 pour un code haute performance. AFAIK, x64 JIT est basé sur le compilateur C ++, et fait des choses amusantes comme les optimisations de récursion en queue.
  4. Avec 4.5, vous pouvez également profiter de l'optimisation guidée par profil et du JIT multi-cœur.
  5. Enfin, vous pouvez utiliser le modèle async / await pour le traiter plus rapidement.

13
2017-10-08 15:11