Linq è più efficiente di quanto appaia in superficie?


13

Se scrivo qualcosa del genere:

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

È lo stesso di:

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);

O c'è della magia sotto le coperte che funziona più in questo modo:

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

O è qualcosa di completamente diverso del tutto?


4
Puoi vederlo in ILSpy.
ChaosPandion,

1
È più simile al secondo esempio che alla prima ma seconda risposta di ChaosPandion che ILSpy è tuo amico.
Michael,

Risposte:


27

Le query LINQ sono pigre . Ciò significa che il codice:

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

fa molto poco. L'enumerabile originale ( mythings) viene enumerato solo quando l'enumerabile risultante ( things) viene consumato, ad esempio da un foreachciclo .ToList(), o .ToArray().

Se chiami things.ToList(), è approssimativamente equivalente al tuo ultimo codice, con forse un certo overhead (di solito insignificante) dagli enumeratori.

Allo stesso modo, se si utilizza un ciclo foreach:

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

È simile nelle prestazioni a:

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

Alcuni dei vantaggi prestazionali dell'approccio alla pigrizia per gli enumerabili (rispetto al calcolo di tutti i risultati e alla loro memorizzazione in un elenco) sono che utilizza pochissima memoria (poiché viene archiviato un solo risultato alla volta) e che non esiste un significativo -front cost.

Se l'enumerabile è elencato solo parzialmente, questo è particolarmente importante. Considera questo codice:

things.First();

Il modo in cui LINQ è implementato, mythingsverrà elencato solo fino al primo elemento che corrisponde alle condizioni dell'utente. Se quell'elemento è all'inizio dell'elenco, questo può essere un enorme aumento delle prestazioni (ad es. O (1) invece di O (n)).


1
Una differenza di prestazioni tra LINQ e il codice equivalente utilizzato foreachè che LINQ utilizza invocazioni delegate, che presentano un certo sovraccarico. Questo può essere significativo quando le condizioni si eseguono molto rapidamente (cosa che fanno spesso).
svick,

2
Questo è ciò che intendevo per sovraccarico dell'enumeratore. Può essere un problema in alcuni (rari) casi, ma nella mia esperienza non è molto frequente - di solito il tempo impiegato è molto piccolo all'inizio, o è ampiamente compensato da altre operazioni che stai facendo.
Cyanfish,

Una brutta limitazione della valutazione pigra di Linq è che non c'è modo di fare una "istantanea" di un elenco se non tramite metodi come ToListo ToArray. Se una cosa del genere fosse stata incorporata correttamente IEnumerable, sarebbe stato possibile chiedere a un elenco di "snapshot" tutti gli aspetti che potrebbero cambiare in futuro senza dover generare tutto.
supercat

7

Il seguente codice:

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

Non equivale a nulla, a causa della valutazione pigra, non accadrà nulla.

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

È diverso, perché la valutazione verrà avviata.

Ogni articolo di mythingssarà dato al primo Where. Se passa, sarà dato al secondo Where. Se passa, farà parte dell'output.

Quindi questo sembra più simile a questo:

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

7

Esecuzione differita a parte (che già spiegano le altre risposte, sottolineo solo un altro dettaglio), è più simile al tuo secondo esempio.

Diciamo solo immaginare che chiamate ToListsu things.

L'implementazione dei Enumerable.Whereresi a Enumerable.WhereListIterator. Quando si chiama Wheresu quella WhereListIterator(aka concatenamento Where-calls), è chiamata non è più Enumerable.Where, ma Enumerable.WhereListIterator.Where, in realtà, che unisce i predicati (usando Enumerable.CombinePredicates).

Quindi è più simile if(t.IsSomeValue && t.IsSomeOtherValue).


"restituisce un Enumerable.WhereListIterator" ha fatto clic per me. Probabilmente un concetto molto semplice, ma è quello che stavo trascurando con ILSpy. Grazie
ConditionRacer,

Guarda la reimplementazione di Jon Skeet di questa ottimizzazione se sei interessato ad un'analisi più approfondita.
Servito il

1

No, non è lo stesso. Nel tuo esempio thingsè un IEnumerable, che a questo punto è ancora solo un iteratore, non un vero array o elenco. Inoltre, poiché thingsnon viene utilizzato, il ciclo non viene mai nemmeno valutato. Il tipo IEnumerableconsente di scorrere gli elementi yieldtramite le istruzioni Linq e di elaborarli ulteriormente con ulteriori istruzioni, il che significa che alla fine hai davvero un solo ciclo.

Ma non appena aggiungi un'istruzione come .ToArray()o .ToList(), stai ordinando la creazione di una struttura dati effettiva, ponendo così limiti alla tua catena.

Vedi questa domanda SO correlata: /programming/2789389/how-do-i-implement-ienumerable

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.