Linq est-il plus efficace qu'il n'y paraît en surface?

13

Si j'écris quelque chose comme ça:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)

Est-ce la même chose que:

var results1 = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue)
        results1.Add(t);

var results2 = new List<Thing>();
foreach(var t in results1)
    if(t.IsSomeOtherValue)
        results2.Add(t);

Ou y a-t-il de la magie sous les couvertures qui fonctionne plus comme ceci:

var results = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue && t.IsSomeOtherValue)
        results.Add(t);

Ou est-ce quelque chose de complètement différent?

ConditionRacer
la source
4
Vous pouvez le voir dans ILSpy.
ChaosPandion
1
Cela ressemble plus au deuxième exemple qu'à la première mais à la deuxième réponse de ChaosPandion selon laquelle ILSpy est votre ami.
Michael

Réponses:

27

Les requêtes LINQ sont paresseuses . Cela signifie que le code:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

fait très peu. L' mythingsénumérateur original ( ) n'est énuméré que lorsque l'énumérateur résultant ( things) est consommé, par exemple par une foreachboucle,, .ToList()ou .ToArray().

Si vous appelez things.ToList(), il est à peu près équivalent à votre dernier code, avec peut-être une surcharge (généralement insignifiante) des énumérateurs.

De même, si vous utilisez une boucle foreach:

foreach (var t in things)
    DoSomething(t);

Ses performances sont similaires à:

foreach (var t in mythings)
    if (t.IsSomeValue && t.IsSomeOtherValue)
        DoSomething(t);

Certains des avantages de performance de l'approche de la paresse pour les énumérables (par opposition au calcul de tous les résultats et à leur stockage dans une liste) sont qu'elle utilise très peu de mémoire (car un seul résultat est stocké à la fois) et qu'il n'y a pas de hausse significative -coût initial.

Si l'énumérable n'est que partiellement énuméré, cela est particulièrement important. Considérez ce code:

things.First();

La façon dont LINQ est implémenté mythingsne sera énumérée que jusqu'au premier élément qui correspond à vos conditions where. Si cet élément est au début de la liste, cela peut être un énorme gain de performances (par exemple O (1) au lieu de O (n)).

Cyanfish
la source
1
Une différence de performances entre LINQ et le code équivalent utilisant foreachest que LINQ utilise des appels de délégué, qui ont une surcharge. Cela peut être important lorsque les conditions s'exécutent très rapidement (ce qu'elles font souvent).
svick
2
C'est ce que je voulais dire par les frais généraux de l'énumérateur. Cela peut être un problème dans certains cas (rares), mais d'après mon expérience, ce n'est pas très fréquent - généralement le temps qu'il faut est très court au début, ou il est largement compensé par les autres opérations que vous effectuez.
Cyanfish
Une mauvaise limitation de l'évaluation paresseuse de Linq est qu'il n'y a aucun moyen de prendre un "instantané" d'une énumération, sauf via des méthodes comme ToListou ToArray. Si une telle chose avait été correctement intégrée IEnumerable, il aurait été possible de demander à une liste de "prendre un instantané" tous les aspects qui pourraient changer à l'avenir sans avoir à tout générer.
supercat
7

Le code suivant:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

Est équivalent à rien, à cause de l'évaluation paresseuse, rien ne se passera.

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)
    .ToList();

Est différent, car l'évaluation sera lancée.

Chaque article mythingssera remis au premier Where. S'il passe, il sera remis au second Where. S'il passe, il fera partie de la sortie.

Donc, cela ressemble plus à ceci:

var results = new List<Thing>();
foreach(var t in mythings)
{
    if(t.IsSomeValue)
    {
        if(t.IsSomeOtherValue)
        {
            results.Add(t);
        }
    }
}
Cyril Gandon
la source
7

Exécution différée mise à part (ce que les autres réponses expliquent déjà, je vais juste souligner un autre détail), c'est plus comme dans votre deuxième exemple.

Imaginons que vous appelez ToListle things.

La mise en œuvre des Enumerable.Whereretours a Enumerable.WhereListIterator. Lorsque vous appelez Wherecela WhereListIterator(alias chaînage Where-appels), vous n'appelez plus Enumerable.Where, mais Enumerable.WhereListIterator.Where, qui combine en fait les prédicats (à l'aide Enumerable.CombinePredicates).

C'est plus comme ça if(t.IsSomeValue && t.IsSomeOtherValue).

la paresse
la source
"renvoie un Enumerable.WhereListIterator" l'a fait cliquer pour moi. Probablement un concept très simple, mais c'est ce que je négligeais avec ILSpy. Merci
ConditionRacer
Voir la réimplémentation de Jon Skeet de cette optimisation si vous êtes intéressé par une analyse plus approfondie.
Servy
1

Non, ce n'est pas pareil. Dans votre exemple , thingsun IEnumerable, qui à ce stade n'est encore qu'un itérateur, pas un tableau ou une liste réelle. De plus puisqu'elle thingsn'est pas utilisée, la boucle n'est même jamais évaluée. Le type IEnumerablepermet d'itérer à travers des éléments yield-ed par des instructions Linq et de les traiter plus loin avec plus d'instructions, ce qui signifie qu'au final vous n'avez vraiment qu'une seule boucle.

Mais dès que vous ajoutez une instruction comme .ToArray()ou .ToList(), vous ordonnez la création d'une structure de données réelle, ce qui met des limites à votre chaîne.

Voir cette question SO connexe: /programming/2789389/how-do-i-implement-ienumerable

Julien Guertault
la source