2012-06-14 15 views
23

Tres implementaciones diferentes para encontrar la suma de IEnumerable < int> source se dan a continuación junto con el tiempo que se tarda cuando la fuente tiene 10.000 enteros.Rendimiento agregado vs suma en LINQ

source.Aggregate(0, (result, element) => result + element); 

tarda de 3 ms

source.Sum(c => c); 

tarda 12 ms

source.Sum(); 

toma 1 ms

Me pregunto por qué la segunda aplicación es cuatro veces más caro que el primero . ¿No debería ser lo mismo que la tercera implementación?

+0

¿Cuáles son sus condiciones de prueba? –

+5

¿cómo conseguiste estos tiempos? ¿Cuántas veces probaste los resultados? –

+0

Lo describí usando dotTrace. Lo ejecuté una vez, pero las tres carreras son independientes. – Gopal

Respuesta

68

Nota: Mi computadora ejecuta .Net 4.5 RC, por lo que es posible que mis resultados se vean afectados por esto.

Medir el tiempo que lleva ejecutar un método solo una vez generalmente no es muy útil. Puede ser dominado fácilmente por cosas como la compilación JIT, que no son cuellos de botella reales en código real. Debido a esto, medí la ejecución de cada método 100 × (en modo Release sin depurador conectado). Mis resultados son:

  • Aggregate(): 9 ms
  • Sum(lambda): 12 ms
  • Sum(): 6 ms

El hecho de que Sum() es el más rápido no es sorprendente: contiene un bucle sencillo sin ninguna invocaciones de delegado, que es realmente rápido. La diferencia entre Sum(lambda) y Aggregate() no es tan prominente como lo que midió, pero aún está allí. ¿Cuál puede ser la razón para esto? Veamos código descompilación de los dos métodos:

public static TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func) 
{ 
    if (source == null) 
     throw Error.ArgumentNull("source"); 
    if (func == null) 
     throw Error.ArgumentNull("func"); 

    TAccumulate local = seed; 
    foreach (TSource local2 in source) 
     local = func(local, local2); 
    return local; 
} 

public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector) 
{ 
    return source.Select<TSource, int>(selector).Sum(); 
} 

Como se puede ver, Aggregate() utiliza un bucle pero Sum(lambda) utiliza Select(), que a su vez utiliza un iterador. Y usar un iterador significa que hay una sobrecarga: crear el objeto iterador y (probablemente más importante) una invocación de método más para cada elemento.

Vamos a verificar que el uso de Select() es realmente la razón por escribir nuestra propia Sum(lambda) dos veces, una vez usando Select(), que debe comportarse de la misma que Sum(lambda) del marco, y una vez sin utilizar Select():

public static int SlowSum<T>(this IEnumerable<T> source, Func<T, int> selector) 
{ 
    return source.Select(selector).Sum(); 
} 

public static int FastSum<T>(this IEnumerable<T> source, Func<T, int> selector) 
{ 
    if (source == null) 
     throw new ArgumentNullException("source"); 
    if (selector == null) 
     throw new ArgumentNullException("selector"); 

    int num = 0; 
    foreach (T item in source) 
     num += selector(item); 
    return num; 
} 

Mis medidas confirmar lo que pensaba:

  • SlowSum(lambda): 12 ms
  • FastSum(lambda): 9 ms
+0

Muy perspicaz. Gracias por la respuesta detallada. – Gopal

+0

Esta es una de las mejores respuestas que he visto en SO, perfectamente explicada y bien elaborada. (+1 obviamente) – RichK