631 votes

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

Il m'arrive souvent de vouloir évaluer une requête à l'endroit même où je la déclare. C'est généralement parce que j'ai besoin d'itérer plusieurs fois sur la requête et il est coûteux à calculer. A titre d'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 n'ai pas l'intention de modifier le résultat, autant appeler ToArray() au lieu de ToList() .

Je me demande toutefois si ToArray() est mis en œuvre en appelant d'abord ToList() et est donc moins efficace en termes de mémoire que l'appel à ToList() .

Suis-je fou ? Dois-je appeler ToArray() - en toute sécurité en sachant que la mémoire ne sera pas allouée deux fois ?

11 votes

Si vous souhaitez découvrir ce qui se passe derrière les rideaux en .NET, je vous recommande vivement .NET Reflector

34 votes

@DavidHedlund Je recommande Code source .net .

1 votes

Je ne suis pas d'accord avec le fait que stackoverflow.com/questions/6750447/c-toarray-performance fait double emploi avec cette question, même s'il existe un lien important entre les deux. L'utilisation de la mémoire (cette question) et les performances (l'autre question) sont des considérations intéressantes et non négligeables. Elles peuvent être décrites séparément, mais elles doivent toutes deux entrer en ligne de compte dans la décision de choisir l'une plutôt que l'autre. Je ne peux recommander aucune des réponses à cette question ou à l'autre comme étant exhaustive. Il y a plusieurs réponses qui, prises ensemble, fournissent une discussion assez complète sur la façon de choisir l'une ou l'autre.

470voto

JaredPar Points 333733

À moins que vous n'ayez simplement besoin d'un tableau pour répondre à d'autres contraintes, vous devriez utiliser ToList . Dans la majorité des scénarios ToArray allouera plus de mémoire que ToList .

Les deux utilisent des tableaux pour le stockage, mais ToList a une contrainte plus souple. Il faut que le tableau soit au moins aussi grand que le nombre d'éléments de la collection. Si le tableau est plus grand, cela ne pose pas de problème. En revanche, si le tableau est plus grand, cela ne pose pas de problème. ToArray a besoin que la taille du tableau corresponde exactement au nombre d'éléments.

Pour répondre à cette contrainte ToArray fait souvent une allocation de plus que ToList . Une fois qu'il dispose d'un tableau suffisamment grand, il alloue un tableau ayant exactement la bonne taille et recopie les éléments dans ce tableau. Le seul cas où il peut éviter cela est celui où l'algorithme de croissance du tableau coïncide avec le nombre d'éléments devant être stockés (ce qui est certainement minoritaire).

EDITAR

Quelques personnes m'ont interrogé sur les conséquences de la présence d'une mémoire supplémentaire inutilisée dans le système d'exploitation. List<T> valeur.

Il s'agit d'une préoccupation légitime. Si la collection créée a une longue durée de vie, n'est jamais modifiée après sa création et a de fortes chances d'atterrir dans le tas Gen2, il est préférable d'utiliser l'allocation supplémentaire de ToArray en amont.

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

L'essentiel est d'établir des profils, des profils et encore des profils.

25 votes

D'un autre côté, la mémoire supplémentaire allouée pour le travail de création du tableau ne serait-elle pas éligible au ramassage des ordures, alors que les frais généraux supplémentaires pour la liste resteraient inchangés ? Je pense qu'il faut rester simple. Si vous avez besoin d'ajouter ou de supprimer des éléments, il existe un outil pour cela. Dans le cas contraire, il y a un autre outil pour cela. Utilisez celui qui a du sens. Si, par la suite, vous découvrez un problème de mémoire et de performance, et voici ce qu'il en est , le modifier.

3 votes

@AnthonyPegram oui, c'est une considération valable à faire. Si la valeur est utilisée pour le stockage à long terme, qu'elle ne sera pas modifiée et qu'elle sera potentiellement intégrée à la génération 2, il est préférable de payer l'allocation supplémentaire maintenant plutôt que de polluer le tas de la génération 2. Cependant, je vois rarement cela. Il est beaucoup plus courant de voir ToArray passer immédiatement à une autre requête LINQ de courte durée.

2 votes

@AnthonyPegram J'ai mis à jour ma réponse pour inclure cet aspect de la discussion.

178voto

mquander Points 32650

La différence de performance sera insignifiante, puisque List<T> est mis en œuvre sous la forme d'un tableau de taille dynamique. L'appel à l'une ou l'autre des fonctions ToArray() (qui utilise un Buffer<T> pour faire croître le tableau) ou ToList() (qui appelle le List<T>(IEnumerable<T>) ) se résumera à les placer dans un tableau et à agrandir ce tableau jusqu'à ce qu'il les contienne tous.

Si vous souhaitez une confirmation concrète de ce fait, vérifiez la mise en œuvre des méthodes en question dans Reflector - vous verrez qu'elles se résument à un code presque identique.

0 votes

Dans Entity-Framework, il y a une option de rechargement rapide en utilisant EntityCollection.CreateSourceQuery, où parfois je rejette les résultats, alors je pense qu'il est plus approprié d'utiliser ToArray.

2 votes

Un fait intéressant que j'ai découvert est que pour les requêtes corrélées causées par l'utilisation d'un groupe défini par une jointure de groupe dans votre projection, Linq to SQL ajoute une autre sous-requête pour récupérer le nombre pour ce groupe. Je suppose que cela signifie que dans ces cas, la taille de la collection est connue avant que les éléments ne soient récupérés et qu'un tableau de taille exacte peut donc être créé directement, ce qui permet d'économiser des ressources de traitement et de mémoire lors de la matérialisation des résultats.

154 votes

Si le comte est connu à l'avance, la performance est identique. Par contre, si le compte n'est pas connu à l'avance, la seule différence entre ToArray() y ToList() est que le premier doit couper l'excédent, ce qui implique de copier l'ensemble du tableau, alors que le second ne coupe pas l'excédent, mais utilise en moyenne 25 % de mémoire en plus. Cela n'a d'incidence que si le type de données est un grand tableau de type struct . Il s'agit simplement d'une piste de réflexion.

30voto

EMP Points 17246

Je suis d'accord avec @mquander pour dire que la différence de performance devrait être insignifiante. Cependant, j'ai voulu faire une analyse comparative pour m'en assurer, et c'est ce que j'ai fait - et c'est effectivement 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/liste source comporte 1000 éléments. Vous pouvez donc constater que les différences de temps et de mémoire sont négligeables.

Ma conclusion : vous pourriez tout aussi bien utiliser ToList() puisque a List<T> offre plus de fonctionnalités qu'un tableau, à moins que quelques octets de mémoire ne soient vraiment importants pour vous.

1 votes

Je me demande si ce résultat serait différent si l'on utilisait un grand struct au lieu d'un type primitif ou d'une classe.

13 votes

List<T>.ToList ? ??? Quel sens ? Il faut essayer de lui donner un IEnumerable, qui n'implémente pas l'interface ICollection.

9 votes

Je voulais m'assurer que je ne mesurais que la durée de la ToList o ToArray et non l'énumération d'un quelconque IEnumerable . List<T>.ToList() crée toujours une nouvelle List<T> - elle ne se contente pas de "renvoyer ceci".

23voto

Vitaliy Ulantikov Points 2834

ToList() est généralement préféré 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épart, ToArray() crée une collection de longueur dynamique comme List, puis la convertit en tableau, ce qui prend plus de temps.

29 votes

J'ai décidé que la lisibilité l'emportait sur la performance dans ce cas. Je n'utilise désormais ToList que lorsque je prévois de continuer à ajouter des éléments. Dans tous les autres cas (la plupart des cas), j'utilise ToArray. Mais merci pour votre contribution !

7 votes

Regarder dans ILSpy, Enumerable.ToArray() appels new Buffer<TSource>(source).ToArray() . Dans le constructeur du tampon, si la source implémente ICollection, elle appelle source.CopyTo(items, 0), puis .ToArray() renvoie directement le tableau interne d'items. Il n'y a donc pas de conversion qui prenne du temps dans ce cas. Si la source n'implémente pas ICollection, le ToArray aboutira à une copie du tableau afin d'éliminer les emplacements inutilisés de la fin du tableau, comme décrit dans le commentaire de Scott Rippey ci-dessus.

22voto

Guffa Points 308133

La mémoire sera toujours allouée deux fois - ou quelque chose d'approchant. Comme il n'est pas possible de redimensionner un tableau, les deux méthodes utiliseront une sorte de mécanisme pour rassembler les données dans une collection croissante. (En fait, la liste est une collection croissante en elle-même).

La liste utilise une matrice comme stockage interne et double la capacité en cas de besoin. Cela signifie qu'en moyenne, 2/3 des éléments ont été réaffectés au moins une fois, la moitié au moins deux fois, la moitié au moins trois fois, et ainsi de suite. Cela signifie que chaque élément a été réaffecté en moyenne 1,3 fois, ce qui n'est pas très élevé.

N'oubliez pas non plus que si vous collectez des chaînes de caractères, 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éallouées.

0 votes

@JonofAllTrades : Non, le tableau n'est jamais étendu sur place, la gestion de la mémoire dans .NET ne le permet tout simplement pas. S'il était étendu en place, il ne serait pas nécessaire de réaffecter les éléments.

1 votes

Ah, je vois : les postes qui ne sont pas réaffectés ne devaient pas l'être parce qu'ils faisaient partie de l'allocation finale. Tous les éléments alloués dans les allocations précédentes sont déplacés, mais en raison de l'augmentation logarithmique de la longueur du tableau, il s'agit d'une fraction calculable. Merci pour cet éclaircissement !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X