2012-06-14 22 views
6

Conozco la diferencia entre IQueryable e IEnumerable, y sé que las colecciones son compatibles con Linq To Objects a través de la interfaz IEnumerable.Por qué IQueryable es dos veces más rápido que IEnumerable al usar Linq To Objects

Lo que me desconcierta es que las consultas se ejecutan dos veces más rápido cuando la colección se convierte a IQueryable.

Deje l ser un objeto relleno de tipo Lista, a continuación, una consulta LINQ es tiempo dos veces más rápido si la lista l se convierte en un IQueryable través l.AsQueryable().

He escrito una prueba simple con VS2010SP1 y .NET 4.0 que demuestra esto:

private void Test() 
{ 
    const int numTests = 1; 
    const int size = 1000 * 1000; 
    var l = new List<int>(); 
    var resTimesEnumerable = new List<long>(); 
    var resTimesQueryable = new List<long>(); 
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); 

    for (int x=0; x<size; x++) 
    { 
    l.Add(x); 
    } 

    Console.WriteLine("Testdata size: {0} numbers", size); 
    Console.WriteLine("Testdata iterations: {0}", numTests); 

    for (int n = 0; n < numTests; n++) 
    { 
    sw.Restart(); 
    var result = from i in l.AsEnumerable() where (i % 10) == 0 && (i % 3) != 0 select i; 
    result.ToList(); 
    sw.Stop(); 
    resTimesEnumerable.Add(sw.ElapsedMilliseconds); 
    } 
    Console.WriteLine("TestEnumerable"); 
    Console.WriteLine(" Min: {0}", Enumerable.Min(resTimesEnumerable)); 
    Console.WriteLine(" Max: {0}", Enumerable.Max(resTimesEnumerable)); 
    Console.WriteLine(" Avg: {0}", Enumerable.Average(resTimesEnumerable)); 

    for (int n = 0; n < numTests; n++) 
    { 
    sw.Restart(); 
    var result = from i in l.AsQueryable() where (i % 10) == 0 && (i % 3) != 0 select i; 
    result.ToList(); 
    sw.Stop(); 
    resTimesQueryable.Add(sw.ElapsedMilliseconds); 
    } 
    Console.WriteLine("TestQuerable"); 
    Console.WriteLine(" Min: {0}", Enumerable.Min(resTimesQueryable)); 
    Console.WriteLine(" Max: {0}", Enumerable.Max(resTimesQueryable)); 
    Console.WriteLine(" Avg: {0}", Enumerable.Average(resTimesQueryable)); 
} 

Al ejecutar esta prueba (con voluntad numTests == 1 y 10) produce el siguiente resultado:

Testdata size: 1000000 numbers 
Testdata iterations: 1 
TestEnumerable 
    Min: 44 
    Max: 44 
    Avg: 44 
TestQuerable 
    Min: 37 
    Max: 37 
    Avg: 37 

Testdata size: 1000000 numbers 
Testdata iterations: 10 
TestEnumerable 
    Min: 22 
    Max: 29 
    Avg: 23,9 
TestQuerable 
    Min: 12 
    Max: 22 
    Avg: 13,9 

Repitiendo la prueba pero cambiando el orden (es decir, primero midiendo IQuerable y luego IEnumerable) da resultados diferentes!

Testdata size: 1000000 numbers 
Testdata iterations: 1 
TestQuerable 
    Min: 75 
    Max: 75 
    Avg: 75 
TestEnumerable 
    Min: 25 
    Max: 25 
    Avg: 25 

Testdata size: 1000000 numbers 
Testdata iterations: 10 
TestQuerable 
    Min: 12 
    Max: 28 
    Avg: 14 
TestEnumerable 
    Min: 22 
    Max: 26 
    Avg: 23,4 

Aquí están mis preguntas:

  1. ¿Qué estoy haciendo mal?
  2. ¿Por qué es IEnumerable más rápido si la prueba se ejecuta después de IQueryable test?
  3. ¿Por qué es IQueryable más rápido cuando el no. de carreras de prueba se incrementa?
  4. ¿Hay una penalización involucrada al usar IQueryable en lugar de IEnumerable?

Hago estas preguntas porque me preguntaba cuál utilizar para mi interfaz de repositorio. En este momento consultan las colecciones en la memoria (Linq to Objects), pero en el futuro este podría ser ser un origen de datos SQL. Si diseño las clases de repositorio ahora con IQueryable puedo cambiar sin más tarde a Linq a SQL. Sin embargo, si se aplica una penalización de rendimiento, entonces, si metemos a IEnumerable, si bien no hay SQL involucrado, parece ser más inteligente.

+0

No está especificando si está construyendo en modo de liberación o depuración, y no está "imprimiendo" las funciones primero para que pueda ver ruido de fluctuación. (Creo). A la larga, una diferencia de unos pocos ms más de 10 000 000 iteraciones en realidad no parece ser un gran problema. – asawyer

+0

Estaba construyendo en modo de depuración. Cambiar al modo de lanzamiento y agregar una ejecución de "inicialización" (es decir, ejecutar y materializar cada consulta una vez) ayudó: Ahora el código ** IEnumerable ** se ejecuta un poco más rápido que el código ** IQueryable ** (11ms vs 12ms). Y eso es exactamente lo que esperaba. Así que mi código de prueba fue defectuoso. Gracias por los consejos! – rbu

+0

"Puedo cambiar luego sin problemas a Linq a SQL" ... Me encantaría saber cómo te fue. –

Respuesta

5

Usando LINQPad para examinar el código IL, aquí es lo que estoy viendo:

Para que este código:

var l = Enumerable.Range(0,100); 

var result = from i in l.AsEnumerable() where (i % 10) == 0 && (i % 3) != 0 select i; 

Esto se genera:

IL_0001: ldc.i4.0  
IL_0002: ldc.i4.s 64 
IL_0004: call  System.Linq.Enumerable.Range 
IL_0009: stloc.0  
IL_000A: ldloc.0  
IL_000B: call  System.Linq.Enumerable.AsEnumerable 
IL_0010: ldsfld  UserQuery.CS$<>9__CachedAnonymousMethodDelegate1 
IL_0015: brtrue.s IL_002A 
IL_0017: ldnull  
IL_0018: ldftn  b__0 
IL_001E: newobj  System.Func<System.Int32,System.Boolean>..ctor 
IL_0023: stsfld  UserQuery.CS$<>9__CachedAnonymousMethodDelegate1 
IL_0028: br.s  IL_002A 
IL_002A: ldsfld  UserQuery.CS$<>9__CachedAnonymousMethodDelegate1 
IL_002F: call  System.Linq.Enumerable.Where 
IL_0034: stloc.1  

b__0: 
IL_0000: ldarg.0  
IL_0001: ldc.i4.s 0A 
IL_0003: rem   
IL_0004: brtrue.s IL_0011 
IL_0006: ldarg.0  
IL_0007: ldc.i4.3  
IL_0008: rem   
IL_0009: ldc.i4.0  
IL_000A: ceq   
IL_000C: ldc.i4.0  
IL_000D: ceq   
IL_000F: br.s  IL_0012 
IL_0011: ldc.i4.0  
IL_0012: stloc.0  
IL_0013: br.s  IL_0015 
IL_0015: ldloc.0  
IL_0016: ret   

Y para este código:

var l = Enumerable.Range(0,100); 

var result = from i in l.AsQueryable() where (i % 10) == 0 && (i % 3) != 0 select i; 

We conseguir esto:

IL_0001: ldc.i4.0  
IL_0002: ldc.i4.s 64 
IL_0004: call  System.Linq.Enumerable.Range 
IL_0009: stloc.0  
IL_000A: ldloc.0  
IL_000B: call  System.Linq.Queryable.AsQueryable 
IL_0010: ldtoken  System.Int32 
IL_0015: call  System.Type.GetTypeFromHandle 
IL_001A: ldstr  "i" 
IL_001F: call  System.Linq.Expressions.Expression.Parameter 
IL_0024: stloc.2  
IL_0025: ldloc.2  
IL_0026: ldc.i4.s 0A 
IL_0028: box   System.Int32 
IL_002D: ldtoken  System.Int32 
IL_0032: call  System.Type.GetTypeFromHandle 
IL_0037: call  System.Linq.Expressions.Expression.Constant 
IL_003C: call  System.Linq.Expressions.Expression.Modulo 
IL_0041: ldc.i4.0  
IL_0042: box   System.Int32 
IL_0047: ldtoken  System.Int32 
IL_004C: call  System.Type.GetTypeFromHandle 
IL_0051: call  System.Linq.Expressions.Expression.Constant 
IL_0056: call  System.Linq.Expressions.Expression.Equal 
IL_005B: ldloc.2  
IL_005C: ldc.i4.3  
IL_005D: box   System.Int32 
IL_0062: ldtoken  System.Int32 
IL_0067: call  System.Type.GetTypeFromHandle 
IL_006C: call  System.Linq.Expressions.Expression.Constant 
IL_0071: call  System.Linq.Expressions.Expression.Modulo 
IL_0076: ldc.i4.0  
IL_0077: box   System.Int32 
IL_007C: ldtoken  System.Int32 
IL_0081: call  System.Type.GetTypeFromHandle 
IL_0086: call  System.Linq.Expressions.Expression.Constant 
IL_008B: call  System.Linq.Expressions.Expression.NotEqual 
IL_0090: call  System.Linq.Expressions.Expression.AndAlso 
IL_0095: ldc.i4.1  
IL_0096: newarr  System.Linq.Expressions.ParameterExpression 
IL_009B: stloc.3  
IL_009C: ldloc.3  
IL_009D: ldc.i4.0  
IL_009E: ldloc.2  
IL_009F: stelem.ref 
IL_00A0: ldloc.3  
IL_00A1: call  System.Linq.Expressions.Expression.Lambda 
IL_00A6: call  System.Linq.Queryable.Where 
IL_00AB: stloc.1  

por lo que parece que la diferencia es la versión AsQuerable está construyendo un árbol de expresión, AsEnumerable no.

+1

Sí, la diferencia entre IEnumerable e IQueriable es que la primera usa delegados mientras que la segunda usa árboles de expresión. Pero esto no responde ninguna de mis preguntas :-( – rbu

Cuestiones relacionadas