2012-04-25 10 views
7

Supongamos que tengo el siguiente código:¿Calcula los valores calculados en caché LINQ?

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
"); 
Enumerable.Range (1, 100) 
    .Select (s => X.Elements() 
     .Select (t => Int32.Parse (t.Attribute ("v").Value)) 
     .Aggregate (s, (t, u) => t * u) 
    ) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

¿Cuál es el tiempo de ejecución de .NET realidad haciendo aquí? ¿Está analizando y convirtiendo los atributos en enteros cada una de las 100 veces, o es lo suficientemente inteligente como para darse cuenta de que debe almacenar en caché los valores analizados y no repetir el cálculo para cada elemento en el rango?

Por otra parte, ¿cómo podría averiguar algo así?

Gracias de antemano por su ayuda.

+2

"cómo podría averiguar algo como esto yo mismo" - la mejor oportunidad es estudiar el IL generado a partir de este código. – Andrey

+1

Puede establecer un punto de interrupción del depurador en el método Parse() y ver la frecuencia con la que golpea. –

Respuesta

2

ha sido un tiempo desde que abrí paso por este código, pero, IIRC, la forma en que funciona es Select simplemente almacenar en caché el Func usted suministra y ejecutarlo en la colección de origen de uno en uno. Entonces, para cada elemento en el rango externo, ejecutará la secuencia interna Select/Aggregate como si fuera la primera vez. No hay ningún almacenamiento en caché incorporado, debería implementarlo usted mismo en las expresiones.

Si quería resolver esto usted mismo, usted tiene tres opciones básicas:

  1. compilar el código y utilizar ildasm para ver la IL; es el más preciso pero, especialmente con lambdas y cierres, lo que obtienes de IL puede parecerse a lo que pones en el compilador de C#.
  2. Use algo como dotPeek para descompilar System.Linq.dll en C#; nuevamente, lo que obtienes de este tipo de herramientas puede parecerse al código fuente original, pero al menos será C# (y dotPeek en particular hace un trabajo bastante bueno, y es gratis.)
  3. Mi preferencia personal - descargue .NET 4.0 Reference Source y búsquelo usted mismo; esto es para :) Tienes que confiar en MS que la fuente de referencia coincide con la fuente real utilizada para producir los binarios, pero no veo ninguna razón para dudar de ellos.
  4. Como lo señala @AllonGuralnek, puede establecer puntos de interrupción en expresiones lambda específicas dentro de una sola línea; coloque el cursor en algún lugar dentro del cuerpo de la lambda y presione F9 y se cortará solo la lambda. (Si lo haces mal, será resaltar la línea entera en el color de punto de interrupción, y si lo haces bien, se acaba de poner de relieve la lambda.)
+0

Gracias por la respuesta. Probaré los primeros y terceros métodos. – Shredderroy

+2

4. Coloque el cursor después de '=>' y presione F9. Eso pondrá un punto de interrupción dentro de la lambda y se romperá cuando lo alcance. Repita para cada lambda y obtendrá un buen rastro de lo que se llama cuando. –

+0

@AllonGuralnek ese es un buen punto, tiendo a olvidarme de las lambdas porque usualmente uso el mouse para configurarlas :) –

4

LINQ y IEnumerable<T> es tirón basado. Esto significa que los predicados y acciones que son parte de la declaración LINQ en general no se ejecutan hasta que se extraen los valores. Además, los predicados y las acciones se ejecutarán cada vez que se extraen los valores (por ejemplo, no hay almacenamiento en caché secreto).

Tirando de un IEnumerable<T> se realiza mediante la declaración foreach que en realidad es el azúcar sintáctica para conseguir un empadronador llamando IEnumerable<T>.GetEnumerator() y repetidamente llamar IEnumerator<T>.MoveNext() para tirar de los valores.

operadores LINQ como ToList(), ToArray(), ToDictionary() y ToLookup() envuelve una declaración foreach por lo que estos métodos harán un tirón. Lo mismo puede decirse de operadores como Aggregate(), Count() y First(). Estos métodos tienen en común que producen un solo resultado que debe crearse ejecutando una instrucción foreach.

Muchos operadores LINQ producen una nueva secuencia IEnumerable<T>. Cuando se extrae un elemento de la secuencia resultante, el operador extrae uno o más elementos de la secuencia de origen. El operador Select() es el ejemplo más obvio, pero otros ejemplos son SelectMany(), Where(), Concat(), Union(), Distinct(), Skip() y Take(). Estos operadores no hacen ningún almacenamiento en caché. Cuando el elemento N'th se extrae de un Select(), extrae el elemento N-ésimo de la secuencia de origen, aplica la proyección con la acción suministrada y la devuelve. Nada secreto pasando aquí.

Otros operadores LINQ también producen nuevas secuencias IEnumerable<T>, pero se implementan al extraer realmente toda la secuencia de origen, hacer su trabajo y luego producir una nueva secuencia. Estos métodos incluyen Reverse(), OrderBy() y GroupBy(). Sin embargo, la extracción realizada por el operador solo se lleva a cabo cuando se tira del operador, lo que significa que aún necesita un bucle foreach "al final" de la instrucción LINQ antes de ejecutar cualquier cosa. Podría argumentar que estos operadores usan una memoria caché porque extraen inmediatamente toda la secuencia fuente. Sin embargo, este caché se genera cada vez que se itera el operador, por lo que es realmente un detalle de implementación y no algo que detectará mágicamente que está aplicando la misma operación OrderBy() varias veces en la misma secuencia.


En su ejemplo la ToList() hará un tirón. La acción en el exterior Select se ejecutará 100 veces. Cada vez que se ejecuta esta acción, el Aggregate() hará otra extracción que analizará los atributos XML. En total, su código llamará al Int32.Parse() 200 veces.

Puede mejorar esta tirando de los atributos de una vez en lugar de en cada iteración:

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
") 
.Elements() 
.Select (t => Int32.Parse (t.Attribute ("v").Value)) 
.ToList(); 
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

Ahora Int32.Parse() sólo se le llama 2 veces. Sin embargo, el costo es que una lista de valores de atributo debe asignarse, almacenarse y eventualmente recolectarse como basura. (No es un gran preocupación cuando la lista contiene dos elementos.)

Tenga en cuenta que si se olvida la primera ToList() que tira de los atributos del código seguirá funcionando pero con exactamente las mismas características de rendimiento como el código original. No se utiliza espacio para almacenar los atributos, pero se analizan en cada iteración.

+0

Muchas gracias por la respuesta detallada. – Shredderroy

Cuestiones relacionadas