Eu tenho uma classe, assim:
public class MyClass
{
public int Value { get; set; }
public bool IsValid { get; set; }
}
Na verdade, é muito maior, mas isso recria o problema (esquisitice).
Eu quero obter a soma do Value
, onde a instância é válida. Até agora, encontrei duas soluções para isso.
O primeiro é o seguinte:
int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
O segundo, no entanto, é o seguinte:
int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
Eu quero obter o método mais eficiente. A princípio, pensei que o segundo seria mais eficiente. Então a parte teórica de mim começou a dizer "Bem, um é O (n + m + m), o outro é O (n + n). O primeiro deve ter um desempenho melhor com mais inválidos, enquanto o segundo deve ter um desempenho melhor com menos". Eu pensei que eles teriam um desempenho igual. EDIT: E então @Martin apontou que o Where e o Select foram combinados, portanto deve ser O (m + n). No entanto, se você olhar abaixo, parece que isso não está relacionado.
Então eu testei.
(São mais de 100 linhas, então pensei que era melhor publicá-lo como um Gist.)
Os resultados foram ... interessantes.
Com tolerância de empate de 0%:
As escalas são a favor Select
e Where
, em cerca de ~ 30 pontos.
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36
Com 2% de tolerância de empate:
É o mesmo, exceto que para alguns eles estavam dentro de 2%. Eu diria que é uma margem mínima de erro. Select
e Where
agora tem apenas uma vantagem de ~ 20 pontos.
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37
Com 5% de tolerância de empate:
Isso é o que eu diria ser a minha margem máxima de erro. Isso torna um pouco melhor para o Select
, mas não muito.
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31
Com 10% de tolerância de empate:
Isso está fora da minha margem de erro, mas ainda estou interessado no resultado. Porque dá a Select
e Where
a vantagem de vinte pontos que já há algum tempo.
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21
Com 25% de tolerância de empate:
Esta é a maneira, maneira fora da minha margem de erro, mas eu ainda estou interessado no resultado, porque o Select
e Where
ainda (quase) manter sua vantagem de 20 pontos. Parece que ele está superando em poucos, e é isso que está dando a liderança.
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0
Agora, eu estou supondo que a liderança de 20 pontos veio do meio, onde ambos são obrigados a ficar em torno do mesmo desempenho. Eu poderia tentar registrá-lo, mas seria uma carga de informações para absorver. Um gráfico seria melhor, eu acho.
Então foi o que eu fiz.
Isso mostra que a Select
linha se mantém estável (esperada) e que a Select + Where
linha sobe (esperada). No entanto, o que me intriga é o motivo pelo qual ele não se encontra com os Select
50 ou mais anteriores: na verdade, eu esperava mais de 50, pois um enumerador extra precisava ser criado para o Select
e Where
. Quero dizer, isso mostra a vantagem de 20 pontos, mas não explica o porquê. Acho que esse é o ponto principal da minha pergunta.
Por que se comporta assim? Devo confiar nisso? Caso contrário, devo usar o outro ou este?
Como o @KingKong mencionado nos comentários, você também pode usar Sum
a sobrecarga de uma lambda. Então, minhas duas opções agora foram alteradas para isso:
Primeiro:
int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
Segundo:
int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);
Vou torná-lo um pouco mais curto, mas:
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0
A liderança de vinte pontos ainda está lá, o que significa que não tem a ver com a combinação Where
e Select
apontada por @Marcin nos comentários.
Obrigado por ler minha parede de texto! Além disso, se você estiver interessado, aqui está a versão modificada que registra o CSV que o Excel recebe.
mc.Value
são caros .Where
+Select
não causa duas iterações separadas sobre a coleção de entrada. O LINQ to Objects otimiza-o em uma iteração. Leia mais na minha postagemRespostas:
Select
itera uma vez em todo o conjunto e, para cada item, executa uma ramificação condicional (verificação de validade) e uma+
operação.Where+Select
cria um iterador que ignora elementos inválidos (nãoyield
os faz), executando+
apenas os itens válidos.Portanto, o custo para a
Select
é:E para
Where+Select
:Onde:
p(valid)
é a probabilidade de um item da lista ser válido.cost(check valid)
é o custo da filial que verifica a validadecost(yield)
é o custo de construção do novo estado dowhere
iterador, que é mais complexo que o iterador simples que aSelect
versão usa.Como você pode ver,
n
aSelect
versão é uma constante, enquanto aWhere+Select
versão é uma equação linear comp(valid)
uma variável. Os valores reais dos custos determinam o ponto de interseção das duas linhas e, comocost(yield)
podem ser diferentescost(+)
, eles não necessariamente se cruzam emp(valid)
= 0,5.fonte
cost(append)
? A resposta realmente boa, porém, olha para ela de um ângulo diferente, e não apenas de estatísticas.Where
não cria nada, basta retornar um elemento de cada vez dasource
sequência se apenas preencher o predicado.IEnumerable.Select(IEnumerable, Func)
eIQueryable.Select(IQueryable, Expression<Func>)
. Você está certo que o LINQ não faz "nada" até você iterar sobre a coleção, o que provavelmente é o que você quis dizer.Aqui está uma explicação detalhada do que está causando as diferenças de tempo.
A
Sum()
função para seIEnumerable<int>
parece com isso:Em C #,
foreach
é apenas o açúcar sintático para a versão de iterador da .Net (não deve ser confundida com ) . Portanto, o código acima é realmente traduzido para isso:IEnumerator<T>
IEnumerable<T>
Lembre-se, as duas linhas de código que você está comparando são as seguintes
Agora aqui está o kicker:
O LINQ usa execução adiada . Portanto, embora pareça que
result1
itera sobre a coleção duas vezes, na verdade, apenas itera uma vez. AWhere()
condição é realmente aplicada duranteSum()
, dentro da chamada paraMoveNext()
(Isso é possível graças à magia deyield return
) .Isso significa que, para
result1
, o código dentro dowhile
loop,é executado apenas uma vez para cada item com
mc.IsValid == true
. Por comparação,result2
executará esse código para todos os itens da coleção. É por isso queresult1
geralmente é mais rápido.(No entanto, observe que chamar a
Where()
condição dentroMoveNext()
ainda tem uma pequena sobrecarga, portanto, se a maioria / todos os itens tivermc.IsValid == true
,result2
será realmente mais rápido!)Espero que agora esteja claro por que
result2
geralmente é mais lento. Agora, gostaria de explicar por que afirmei nos comentários que essas comparações de desempenho do LINQ não importam .Criar uma expressão LINQ é barato. Chamar funções de delegado é barato. Alocar e fazer um loop sobre um iterador é barato. Mas é ainda mais barato não fazer essas coisas. Portanto, se você achar que uma instrução LINQ é o gargalo no seu programa, na minha experiência, reescrevê-la sem o LINQ sempre o tornará mais rápido do que qualquer um dos vários métodos LINQ.
Portanto, seu fluxo de trabalho LINQ deve ficar assim:
Felizmente, os gargalos do LINQ são raros. Parreira, gargalos são raros. Escrevi centenas de instruções LINQ nos últimos anos e acabei substituindo <1%. E a maioria deles ocorreu devido à fraca otimização de SQL do LINQ2EF , em vez de ser culpa do LINQ.
Portanto, como sempre, escreva primeiro um código claro e sensível e espere até depois de criar um perfil para se preocupar com as otimizações.
fonte
Coisa engraçada. Você sabe como é
Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)
definido? Ele usaSelect
método!Então, na verdade, tudo deve funcionar quase da mesma forma. Eu fiz uma pesquisa rápida por conta própria, e aqui estão os resultados:
Para as seguintes implementações:
mod
significa: todos os 1mod
itens são inválidos:mod == 1
todos os itens são inválidos,mod == 2
os itens ímpares são inválidos etc. A coleção contém10000000
itens.E resultados para coleta com
100000000
itens:Como você pode ver,
Select
eSum
os resultados são bastante consistentes em todos osmod
valores. No entantowhere
ewhere
+select
não são.fonte
Meu palpite é que a versão com Where filtra os 0s e eles não são um assunto para Sum (ou seja, você não está executando a adição). Obviamente, isso é um palpite, já que não posso explicar como a execução de expressões lambda adicionais e a chamada de vários métodos superam uma simples adição de um 0.
Um amigo meu sugeriu que o fato de o 0 na soma possa causar uma severa penalidade de desempenho devido a verificações de estouro. Seria interessante ver como isso funcionaria em um contexto não verificado.
fonte
unchecked
tornam um pouquinho, um pouquinho melhor para oSelect
.Executando o exemplo a seguir, fica claro para mim que o único momento em que o Where + Select pode superar o Select é de fato quando ele descarta uma boa quantidade (aproximadamente metade dos meus testes informais) dos itens em potencial na lista. No pequeno exemplo abaixo, recebo aproximadamente os mesmos números de ambas as amostras quando o Where ignora aproximadamente 4mil itens de 10mil. Corri na versão e reordenou a execução de where + select vs select com os mesmos resultados.
fonte
Select
?Se você precisar de velocidade, apenas fazer um loop direto é provavelmente a sua melhor aposta. E fazer
for
tende a ser melhor do queforeach
(supondo que sua coleção seja de acesso aleatório, é claro).Aqui estão os horários que recebi com 10% dos elementos inválidos:
E com 90% de elementos inválidos:
E aqui está o meu código de referência ...
Aliás, concordo com o palpite de Stilgar : as velocidades relativas dos seus dois casos variam dependendo da porcentagem de itens inválidos, simplesmente porque a quantidade de trabalho que
Sum
precisa ser feita varia no caso "Onde".fonte
Em vez de tentar explicar por descrição, vou adotar uma abordagem mais matemática.
Dado o código abaixo, que deve aproximar o que o LINQ está fazendo internamente, os custos relativos são os seguintes:
Selecione apenas:
Nd + Na
Onde + Selecione:
Nd + Md + Ma
Para descobrir o ponto em que eles cruzarão, precisamos fazer uma pequena álgebra:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)
O que isso significa é que, para que o ponto de inflexão esteja em 50%, o custo de uma chamada de delegado deve ser aproximadamente o mesmo que o custo de uma adição. Como sabemos que o ponto de inflexão real era de cerca de 60%, podemos trabalhar para trás e determinar que o custo de uma chamada de delegado para @ It'sNotALie era na verdade cerca de 2/3 do custo de uma adição que é surpreendente, mas é isso que dizem seus números.
fonte
Acho interessante que o resultado de MarcinJuraszek seja diferente do de It'sNotALie. Em particular, os resultados de MarcinJuraszek começam com todas as quatro implementações no mesmo local, enquanto os resultados de It'sNotALie se cruzam no meio. Vou explicar como isso funciona a partir da fonte.
Vamos assumir que existem
n
elementos totais em
elementos válidos.A
Sum
função é bem simples. Ele apenas percorre o enumerador: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)Para simplificar, vamos assumir que a coleção é uma lista. Tanto o Select quanto o WhereSelect criarão um
WhereSelectListIterator
. Isso significa que os iteradores reais gerados são os mesmos. Nos dois casos, existe umSum
loop que passa por um iterador, oWhereSelectListIterator
. A parte mais interessante do iterador é o método MoveNext .Como os iteradores são iguais, os loops são os mesmos. A única diferença está no corpo dos loops.
O corpo dessas lambdas tem um custo muito semelhante. A cláusula where retorna um valor de campo e o predicado ternário também retorna um valor de campo. A cláusula select retorna um valor de campo e as duas ramificações do operador ternário retornam um valor de campo ou uma constante. A cláusula select combinada tem o ramo como um operador ternário, mas o WhereSelect utiliza o ramo em
MoveNext
.No entanto, todas essas operações são bastante baratas. A operação mais cara até agora é a filial, onde uma previsão errada nos custará.
Outra operação cara aqui é a
Invoke
. A chamada de uma função demora um pouco mais do que a adição de um valor, como mostrou o Branko Dimitrijevic.Também pesando é a acumulação verificada
Sum
. Se o processador não tiver um sinalizador de estouro aritmético, também pode ser caro verificar isso.Portanto, os custos interessantes são: é:
n
+m
) * Invocar +m
*checked+=
n
* Invocar +n
*checked+=
Portanto, se o custo de Invoke for muito maior que o custo da acumulação verificada, o caso 2 será sempre melhor. Se eles são iguais, veremos um equilíbrio quando cerca de metade dos elementos forem válidos.
Parece que no sistema de MarcinJuraszek, marcado + = tem custo insignificante, mas nos sistemas de It'sNotALie e Branko Dimitrijevic, marcado + = tem custos significativos. Parece que é o mais caro do sistema It'sNotALie, já que o ponto de equilíbrio é muito maior. Parece que ninguém postou resultados de um sistema em que a acumulação custa muito mais que o Invoke.
fonte