Qual é mais rápido: Single (predicado) ou Where (predicado) .Single ()

A discussão resultante dessa resposta me deixa curiosa. O que é mais rápido:

someEnumerable.Single(predicate); 

ou

 someEnumerable.Where(predicate).Single(); 

Afinal, o primeiro é mais curto, mais conciso e parece ser construído de propósito.

Até o ReSharper sugere o primeiro:

insira a descrição da imagem aqui

Eu estava argumentando no post anterior, que eles são funcionalmente idênticos e devem ter tempo de execução muito semelhante.

LINQ para objects

Nada responde a uma pergunta como esta, como uma referência:

(Atualizada)

 class Program { const int N = 10000; volatile private static int s_val; static void DoTest(IEnumerable data, int[] selectors) { Stopwatch s; // Using .Single(predicate) s = Stopwatch.StartNew(); foreach (var t in selectors) { s_val = data.Single(x => x == t); } s.Stop(); Console.WriteLine(" {0} calls to Single(predicate) took {1} ms.", selectors.Length, s.ElapsedMilliseconds); // Using .Where(predicate).Single() s = Stopwatch.StartNew(); foreach (int t in selectors) { s_val = data.Where(x => x == t).Single(); } s.Stop(); Console.WriteLine(" {0} calls to Where(predicate).Single() took {1} ms.", selectors.Length, s.ElapsedMilliseconds); } public static void Main(string[] args) { var R = new Random(); var selectors = Enumerable.Range(0, N).Select(_ => R.Next(0, N)).ToArray(); Console.WriteLine("Using IEnumerable (Enumerable.Range())"); DoTest(Enumerable.Range(0, 10 * N), selectors); Console.WriteLine("Using int[]"); DoTest(Enumerable.Range(0, 10*N).ToArray(), selectors); Console.WriteLine("Using List"); DoTest(Enumerable.Range(0, 10 * N).ToList(), selectors); Console.ReadKey(); } } 

Um pouco chocante, .Where(predicate).Single() ganha por um fator de cerca de dois. Eu até executei os dois casos duas vezes para garantir que o cache, etc., não fosse um fator.

 1) 10000 calls to Single(predicate) took 7938 ms. 1) 10000 calls to Where(predicate).Single() took 3795 ms. 2) 10000 calls to Single(predicate) took 8132 ms. 2) 10000 calls to Where(predicate).Single() took 4318 ms. 

Resultados atualizados:

 Using IEnumerable (Enumerable.Range()) 10000 calls to Single(predicate) took 7838 ms. 10000 calls to Where(predicate).Single() took 8104 ms. Using int[] 10000 calls to Single(predicate) took 8859 ms. 10000 calls to Where(predicate).Single() took 2970 ms. Using List 10000 calls to Single(predicate) took 9523 ms. 10000 calls to Where(predicate).Single() took 3781 ms. 

Where(predicate).Single() seria mais rápido que Singe(predicate)

Edit: Você esperaria que Single() e Single(predicate) fossem codificados de maneira similar, mas esse não é o caso. Single() termina assim que outro elemento é encontrado, mas o último encontra todos os elementos satisfatórios.

Ponto de interesse adicional (resposta original) – Where otimizações especiais para tipos diferentes de tipos de coleção, enquanto outros methods como First , Single e Count não aproveitam o tipo da coleção.

Então, Where(predicate).Single() é capaz de fazer algumas otimizações que Single(predicate) não faz

Com base na implementação real para Where(predicate).Single() e Single(predicate) , parece que o primeiro é realmente preguiçoso, enquanto o último sempre repete todo o IEnumerable . Single() retorna o único elemento da enumeração, mas também testa se a enumeração não possui nenhum ou possui mais de um valor, o que pode ser obtido simplesmente perguntando os próximos 2 elementos da enumeração no máximo. Single(predicate) é atualmente implementado de uma maneira que ele precisa iterar sobre toda a enumeração para confirmar se o predicado é true para um e somente um elemento, portanto, a diferença de desempenho (e funcional, veja abaixo).

Embora pareçam funcionalmente idênticos, há casos em que não apenas o desempenho, mas a funcionalidade real são bem diferentes, ou seja, enumerações infinitas,

 public IEnumerable InfiniteEnumeration() { while (true) { yield return 1; } } 

Se você executar essa function usando os dois methods, um será concluído corretamente; o outro … podemos ter que esperar.

 var singleUsingWhere = InfiniteEnumeration().Where(value => value != 0).Single(); var singleUsingSingle = InfiniteEnumeration().Single(value => value != 0); 

É estranho a Microsoft ter decidido implementar o Single(predicate) desta forma … até Jon Skeet conseguiu consertar essa supervisão .

Há uma falha de design no Single do Linq-for-objects que significa:

  1. É inutilmente manter um registro do número de partidas, em vez de encontrar uma partida e depois jogar se encontrar outra.
  2. Ele continua até atingir o final da sequência, mesmo depois de um terceiro jogo.
  3. Pode lançar OverflowException ; é improvável, mas isso pode ser um bug, no entanto.

https://connect.microsoft.com/VisualStudio/feedback/details/810457/public-static-tsource-single-tsource-this-ienumerable-tsource-source-func-tsource-bool-predicate-doesnt-throw-immediately- no segundo resultado correspondente #

Isso faz com que seja um pouco mais lento no caso de correspondência 0 ou 1 (somente o segundo é o caso sem erro, é claro) e muito mais lento no caso de mais de 1 correspondência (o caso de erro).

Com outros provedores de Linq, isso depende; tende a ser quase o mesmo, mas é perfeitamente possível que um determinado provedor seja menos eficiente com um ou outro e outro provedor dado seja o oposto.

[Edit: Este não é mais o caso do .NET Core, no qual a descrição acima não se aplica mais. Isso faz com que a única chamada para .Single(pred) ligeiramente mais eficiente que .Where(pred).Single() .]

Eu acho que este é um caso de maçãs vs. laranjas.

Temos que pensar como a implementação atual de Single(predicate) difere da seguinte implementação:

 public static TSource Single(this IEnumerable source, Func predicate) { return Where(source, predicate).Single(); } 

A implementação de Where retorna um Enumerable.Iterator que parece reconhecer a condição de corrida que ocorre quando MoveNext é chamado no mesmo iterador em threads diferentes.

De ILSpy:

 switch (this.state) { case 1: this.enumerator = this.source.GetEnumerator(); this.state = 2; break; case 2: break; default: return false; } 

A implementação atual de Single(predicate) e First(predicate) não manipula essa condição.

Estou lutando para pensar no que isso significa em um cenário do mundo real , mas acredito que o “bug” não tenha sido corrigido porque o comportamento seria alterado em determinados cenários de vários segmentos.