¿Es linq más eficiente de lo que parece en la superficie?

13

Si escribo algo como esto:

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

¿Es esto lo mismo 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);

¿O hay algo de magia debajo de las cubiertas que funciona más así:

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

¿O es algo completamente diferente?

CondiciónRacer
fuente
44
Puede ver esto en ILSpy.
ChaosPandion
1
Es más como el segundo ejemplo que la primera pero segunda respuesta de ChaosPandion de que ILSpy es tu amigo.
Michael
2
Consulte también ¿Por qué están superando a Where y Select solo a Select?
BlueRaja - Danny Pflughoeft

Respuestas:

27

Las consultas de LINQ son perezosas . Eso significa el código:

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

hace muy poco El enumerable original ( mythings) solo se enumera cuando thingsse consume el enumerable resultante ( ), por ejemplo, mediante un foreachbucle .ToList(), o .ToArray().

Si llama things.ToList(), es más o menos equivalente a su último código, con quizás una sobrecarga (generalmente insignificante) de los enumeradores.

Del mismo modo, si usa un bucle foreach:

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

Es similar en rendimiento a:

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

Algunas de las ventajas de rendimiento del enfoque de la pereza para los enumerables (en lugar de calcular todos los resultados y almacenarlos en una lista) es que utiliza muy poca memoria (ya que solo se almacena un resultado a la vez) y que no hay un aumento significativo -costo por adelantado.

Si lo enumerable solo se enumera parcialmente, esto es especialmente importante. Considera este código:

things.First();

La forma en que se implementa LINQ mythingssolo se enumerará hasta el primer elemento que coincida con sus condiciones where. Si ese elemento está al principio de la lista, esto puede ser un gran aumento de rendimiento (por ejemplo, O (1) en lugar de O (n)).

Cyanfish
fuente
1
Una diferencia de rendimiento entre LINQ y el código equivalente que usa foreaches que LINQ usa invocaciones de delegado, que tienen algo de sobrecarga. Esto puede ser significativo cuando las condiciones se ejecutan muy rápidamente (lo que a menudo hacen).
svick
2
Eso es lo que quise decir con enumerador de arriba. Puede ser un problema en algunos casos (raros), pero en mi experiencia eso no es muy frecuente, por lo general, el tiempo que lleva es muy pequeño para empezar, o está muy compensado por otras operaciones que está realizando.
Cyanfish
Una desagradable limitación de la evaluación perezosa de Linq es que no hay forma de tomar una "instantánea" de una enumeración, excepto a través de métodos como ToListo ToArray. Si tal cosa se hubiera incorporado correctamente IEnumerable, habría sido posible pedir una lista para "capturar" cualquier aspecto que pudiera cambiar en el futuro sin tener que generar todo.
supercat
7

El siguiente código:

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

Es equivalente a nada, debido a la evaluación perezosa, no pasará nada.

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

Es diferente, porque la evaluación será lanzada.

Cada artículo de mythingsserá entregado al primero Where. Si pasa, se le dará al segundo Where. Si pasa, será parte de la salida.

Entonces esto se parece más a esto:

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

Dejando de lado la ejecución diferida (que las otras respuestas ya explican, solo señalaré otro detalle), es más como en su segundo ejemplo.

Imaginemos que llame ToLista things.

La implementación de Enumerable.Wheredevoluciones a Enumerable.WhereListIterator. Cuando invocas Whereeso WhereListIterator(también conocido como encadenamiento de Wherellamadas), ya no invocas Enumerable.Where, sino Enumerable.WhereListIterator.Whereque, en realidad, combina los predicados (usando Enumerable.CombinePredicates).

Entonces es más como if(t.IsSomeValue && t.IsSomeOtherValue).

perezoso
fuente
"devuelve un Enumerable.WhereListIterator" hizo que haga clic para mí. Probablemente un concepto muy simple, pero eso es lo que estaba pasando por alto con ILSpy. Gracias
ConditionRacer
Vea la reimplementación de Jon Skeet de esta optimización si está interesado en un análisis más profundo.
Servy
1

No, no es lo mismo. En su ejemplo thingses un IEnumerable, que en este punto todavía es solo un iterador, no una matriz o lista real. Además, dado thingsque no se usa, el ciclo nunca se evalúa. El tipo IEnumerablepermite iterar a través de elementos yield-ed por instrucciones de Linq y procesarlos aún más con más instrucciones, lo que significa que al final solo tiene un bucle.

Pero tan pronto como agregue una instrucción como .ToArray()o .ToList(), está ordenando la creación de una estructura de datos real, poniendo límites a su cadena.

Consulte esta pregunta SO relacionada: /programming/2789389/how-do-i-implement-ienumerable

Julien Guertault
fuente