jueves, 22 de octubre de 2009

IList<T>.Count vs IEnumerable<T>.Count() (LINQ)

Te has preguntado alguna vez la diferencia de rendimiento que pueda haber entre el método extensor Count() proporcionado por LINQ y la propiedad Count de la interfaz IList<T>.

Es decir dado el siguiente código:

List<int> lst = new List<int>();
// Añadimos ints a la lista...
// Qué es más rápido?
var count = lst.Count;
var count2 = ((IEnumerable<int>)lst).Count();





A veces hacemos suposiciones sobre como funciona LINQ to objects. Uno puede pensar que el método Count() de LINQ está definido como:




public static int Count<T>(this IEnumerable<T> @this)
{
int count = 0;
foreach (var x in @this) count++;
return count;
}





Hay gente que basándose en estas suposiciornes intenta evitar el uso de Count() cuando sabe que la colección real es una List<T> p.ej. Desgraciadamente esto les lleva a no poder hacer métodos genéricos con IEnumerable<T> (empiezan a trabajar con IList<T>). A veces comentan que usarían mucho más LINQ to Objects, pero que trabajan habitualmente con listas, y que no pueden permitirse el sobrecoste de recorrer toda la lista simplemente para contar los elementos, cuando la clase List<T> ya tiene una propiedad para ello…



… están totalmente equivocados.



LINQ to Objects está optimizado, no es un proveedor tan tonto como algunos piensan… Así realmente si el objeto sobre el que usamos Count() implementa ICollection o ICollection<T>, LINQ usará la propiedad Count directamente, sin recorrer los elementos.



Para que veais que es cierto he realizado un pequeño test:




class Program
{
static void Main(string[] args)
{
List<int> list = new List<int>();
for (int i = 0; i < 10000000; i++)
{
list.Add(i);
}

Stopwatch sw = new Stopwatch();
sw.Start();
CountList(list);
sw.Stop();
Console.WriteLine("List.Count:" + sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
CountLinq(list);
sw.Stop();
Console.WriteLine("LINQ.Count():" + sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
CountLoop(list);
sw.Stop();
Console.WriteLine("foreach count" + sw.ElapsedMilliseconds);
sw.Reset();
Console.ReadLine();
}

static void CountList (IList<int> list)
{
for (int i=0; i< 100; i++)
{
var a = list.Count;
}
}

static void CountLinq(IEnumerable<int> list)
{
for (int i = 0; i < 100; i++)
{
var a = list.Count();
}
}

static void CountLoop(IEnumerable<int> list)
{
for (int i = 0; i < 100; i++)
{
var a = list.Count2();
}
}
}





El test cuenta 100 veces una lista con 10 millones de elementos, y cuenta lo que se tarda usando la propiedad Count de la lista, el método Count() de LINQ y el método Count2, que es un método extensor que recorre la lista (es exactamente el mismo método que he puesto antes).



Los resultados no dejan lugar a dudas:




  1. Usando la propiedad Count, se tarda menos de un ms en contar 100 veces la lista.


  2. Usando el método Count() de LINQ se tarda igualmente menos de un ms en contar la lista 100 veces.


  3. Usando el método extensor Count2 se tarda más de 9 segundos en contar la lista 100 veces…



Si en lugar de 100 veces la contamos diez millones de veces, los resultados son:




  1. 30 ms usando la propiedad Count


  2. 247 ms usando el método Count() de LINQ


  3. Ni idea usando el método extensor Count2… pero vamos si para 100 veces ha tardado 9 segundos… para diez millones… no quiero ni pensarlo!



Los tiempos han sido medidos con la aplicación en Release.



La conclusión es clara: no tengáis miedo a LINQ, que MS no ha hecho algo tan cutre como un triste foreach!! ;-)



Saludos!



PD: En este post del blog del equipo de C# cuentan esta y otras optimizaciones más de LINQ to Objects… lectura imprescindible! :)



PD2: Esto es un crossposting de mi blog geeks.ms!

No hay comentarios: