O linq é mais eficiente do que parece na superfície?

13

Se eu escrever algo como isto:

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

É o mesmo 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 existe alguma mágica embaixo das cobertas que funciona mais assim:

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

Ou é algo completamente diferente?

ConditionRacer
fonte
4
Você pode ver isso no ILSpy.
ChaosPandion
1
É mais como o segundo exemplo do que a primeira, mas segunda resposta do ChaosPandion, de que o ILSpy é seu amigo.
Michael
2
Consulte também Por que o Where e Select está superando apenas o Select?
BlueRaja - Danny Pflughoeft

Respostas:

27

As consultas LINQ são preguiçosas . Isso significa o código:

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

faz muito pouco. O enumerável original ( mythings) só é enumerado quando o enumerável resultante ( things) é consumido, por exemplo, por um foreachloop .ToList(), ou .ToArray().

Se você ligar things.ToList(), é aproximadamente equivalente ao seu último código, talvez com alguma sobrecarga (geralmente insignificante) dos enumeradores.

Da mesma forma, se você usar um loop foreach:

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

É semelhante no desempenho a:

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

Algumas das vantagens de desempenho da abordagem de preguiça para enumeráveis ​​(ao contrário de calcular todos os resultados e armazená-los em uma lista) são que ele usa muito pouca memória (uma vez que apenas um resultado é armazenado por vez) e que não há resultados significativos. custo inicial.

Se o enumerável é apenas parcialmente enumerado, isso é especialmente importante. Considere este código:

things.First();

A maneira como o LINQ é implementado mythingsserá enumerado apenas até o primeiro elemento que corresponda às condições where. Se esse elemento estiver no início da lista, isso pode ser um grande aumento de desempenho (por exemplo, O (1) em vez de O (n)).

Cyanfish
fonte
1
Uma diferença de desempenho entre o LINQ e o código equivalente usado foreaché que o LINQ usa chamadas de delegação, com alguma sobrecarga. Isso pode ser significativo quando as condições são executadas muito rapidamente (o que geralmente ocorre).
svick
2
Isso é o que eu quis dizer com sobrecarga do enumerador. Pode ser um problema em alguns casos (raros), mas, na minha experiência, isso não é muito frequente - geralmente o tempo que leva é muito pequeno para começar ou é superado por outras operações que você está realizando.
Cyanfish
Uma limitação desagradável da avaliação preguiçosa do Linq é que não há como tirar um "instantâneo" de uma enumeração, exceto por métodos como ToListou ToArray. Se tal coisa tivesse sido incorporada adequadamente IEnumerable, seria possível solicitar uma lista para "capturar" todos os aspectos que possam mudar no futuro sem ter que gerar tudo.
precisa
7

O código a seguir:

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

É equivalente a nada, por causa da avaliação preguiçosa, nada vai acontecer.

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

É diferente, porque a avaliação será iniciada.

Cada item mythingsserá entregue ao primeiro Where. Se passar, será dado ao segundo Where. Se passar, fará parte da saída.

Portanto, isso se parece mais com isso:

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

Além da execução adiada (que as outras respostas já explicam, apenas apontarei outro detalhe), é mais como no seu segundo exemplo.

Vamos imaginar que você chama ToListde things.

A implementação de Enumerable.Whereretornos a Enumerable.WhereListIterator. Quando você chama Whereisso WhereListIterator(também conhecido como encadeamento de Wherechamadas), não chama mais Enumerable.Where, mas Enumerable.WhereListIterator.Where, na verdade, combina os predicados (usando Enumerable.CombinePredicates).

Então é mais como if(t.IsSomeValue && t.IsSomeOtherValue).

bicho-preguiça
fonte
"retorna um Enumerable.WhereListIterator" fez clique para mim. Provavelmente um conceito muito simples, mas era isso que eu estava ignorando com o ILSpy. Obrigado
ConditionRacer
Veja a reimplementação dessa otimização por Jon Skeet, se você estiver interessado em uma análise mais aprofundada.
Servy
1

Não, não é a mesma coisa. No seu exemplo, thingsé um IEnumerable, que neste momento ainda é apenas um iterador, não uma matriz ou lista real. Além disso, como thingsnão é usado, o loop nunca é avaliado. O tipo IEnumerablepermite iterar através dos elementos yield-ed pelas instruções do Linq e processá-los ainda mais com mais instruções, o que significa que, no final, você realmente tem apenas um loop.

Mas assim que você adiciona uma instrução como .ToArray()ou .ToList(), você está ordenando a criação de uma estrutura de dados real, colocando assim limites à sua cadeia.

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

Julien Guertault
fonte