Que es más rápido: Single (predicado) o Where (predicado) .Single ()

La discusión resultante de esta respuesta me tiene curioso. Cual es mas rápido:

someEnumerable.Single(predicate); 

o

 someEnumerable.Where(predicate).Single(); 

Después de todo, el primero es más corto, más conciso y parece tener un propósito específico.

Incluso ReSharper sugiere lo primero:

introduzca la descripción de la imagen aquí

En el post anterior, estaba argumentando que son funcionalmente idénticos y que deberían tener un tiempo de ejecución muy similar.

LINQ-to-Objects

Nada responde a una pregunta como esta como un punto de referencia:

(Actualizado)

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

Algo impactante, .Where(predicate).Single() gana por un factor de aproximadamente dos. Incluso corrí ambos casos dos veces para asegurarme de que el almacenamiento en caché, etc. no fue un factor.

 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 actualizados:

 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() sería más rápido que Singe(predicate)

Edición: Usted esperaría que Single() y Single(predicate) se codifiquen de manera similar, pero ese no es el caso. Single() termina tan pronto como se encuentra otro elemento, pero este último encuentra todos los elementos satisfactorios.

Punto de interés adicional (respuesta original): Where se realizan optimizaciones especiales para diferentes tipos de colección, mientras que otros métodos como First , Single y Count no aprovechan el tipo de colección.

Entonces Where(predicate).Single() es capaz de hacer algunas optimizaciones que Single(predicate) no puede

Sobre la base de la implementación real de Where(predicate).Single() y Single(predicate) , parece que el primero en realidad es perezoso, mientras que el último siempre se repite en todo el IEnumerable . Single() devuelve el único elemento de la enumeración, pero también prueba si la enumeración no tiene ninguno o tiene más de un valor, lo que puede lograrse simplemente preguntando los siguientes 2 elementos de la enumeración como máximo. Single(predicate) se implementa actualmente de una manera que necesita iterar sobre toda la enumeración para confirmar si el predicado es true para uno y solo un elemento, por lo tanto, la diferencia de rendimiento (y funcional, ver más abajo).

Aunque parecen funcionalmente idénticos, hay casos en los que no solo el rendimiento, sino la funcionalidad real son bastante diferentes, es decir, enumeraciones infinitas,

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

Si ejecuta esta función utilizando ambos métodos, uno se completará correctamente; El otro … podríamos tener que esperar.

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

Es extraño que Microsoft haya decidido implementar Single(predicate) esta manera … incluso Jon Skeet logró arreglar esa supervisión .

Hay un defecto de diseño en el Linq-for-objects Single que significa:

  1. Es inútil mantener un conteo de la cantidad de coincidencias, en lugar de encontrar una coincidencia y luego lanzarla si encuentra otra.
  2. Sigue avanzando hasta que llega al final de la secuencia, incluso después de un tercer partido.
  3. Puede lanzar OverflowException ; Es poco probable que lo sea, pero que puede ser un error no obstante.

https://connect.microsoft.com/VisualStudio/feedback/details/810457/public-static-tsource-single-tsource-this-ienumerable-source-func-tsource-bool-predicate-doesnt-throw-imm inmediatamente- en el segundo resultado coincidente #

Esto lo hace ligeramente más lento en el caso de una coincidencia de 0 o 1 (por supuesto, solo el segundo es el caso de no error) y mucho más lento en el caso de más de una coincidencia (el caso de error).

Con otros proveedores de Linq, depende; Tiende a ser más o menos igual, pero es perfectamente posible que un proveedor determinado sea menos eficiente con uno u otro y que otro proveedor sea lo contrario.

[Editar: Este ya no es el caso con .NET Core, en el que ya no se aplica la descripción anterior. Esto hace que la llamada única a .Single(pred) un poco más eficiente que .Where(pred).Single() .]

Creo que este es un caso de manzanas vs. naranjas.

Tenemos que pensar cómo la implementación actual de Single(predicate) difiere de la siguiente implementación:

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

La implementación de Where devuelve un Enumerable.Iterator que parece reconocer la condición de carrera que ocurre cuando se llama a MoveNext en el mismo iterador en diferentes subprocesos.

De ILSpy:

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

La implementación actual de Single(predicate) y First(predicate) no maneja esta condición.

Estoy luchando para pensar en lo que esto significa en un escenario del mundo real , pero supongo que el “error” no se ha solucionado porque el comportamiento se alteraría en ciertos escenarios de múltiples subprocesos.