Una colección que implementa la interfaz IEnumerable contiene un método GetEnumerator y este a su vez implementa la interfaz IEnumerator que contiene la propiedad Current y los métodos MoveNext y Reset. Esto va a permitir a la instrucción foreach iterar por la colección.
Así, si generamos una colección de 20.000.000 de números enteros y queremos efectuar una operación por cada uno de ellos, en este caso el inverso del producto de la raiz cuadrada por el logaritmo neperiano y luego realizar la suma sobre cada resultado, podemos iterar con una instrucción foreach,
static int n = 20000000; static List list = Enumerable.Range(1, n).ToList(); static double forEachStandar() { var result = 0.0; foreach (var item in list) { result += 1 / Math.Sqrt(item) * Math.Log(item); } return result; }
Una colección además contiene un método ForEach, el cual es accesible mediante una expresión Lambda, asignando la acción que vamos a realizar en cada uno de los elementos.
static double forEachLambda() { var result = 0.0; list.ForEach(n => result += 1 / Math.Sqrt(n) * Math.Log(n)); return result; }
Podemos aprovechar al máximo los recursos disponibles y es iterando en paralelo por los elementos de la colección pudiendo realizar cálculos simultáneos. Este medio debe ser tratado con cuidado, puesto que si el orden interviene en el resultado final, esta herramienta no sería útil, y en todo caso, los resultados no serían los esperados; así mismo, siempre hay que replantearse el uso en paralelo. Para usarlo, podemos hacerlo mediante la clase Parallel
Parallel.ForEach(list, k => result += 1 / Math.Sqrt(n) * Math.Log(n));
o mediante el método AsParallel de la colección.
foreach (var item in list.AsParallel())
{
result += 1 / Math.Sqrt(item) * Math.Log(item);
}
Aprovechando el artículo sobre LINQ publicado en Wakicode, es posible efectuar cálculos sobre elementos de la colección mediante sintáxis de consulta en Linq. En el siquiente ejemplo, efectuamos el cálculo sobre cada uno de los elementos asignádolo a la variable sum y posteriormente efectuar la suma por cada uno de los elementos
static double queryLinq() { var query = (from u in list select new { sum = 1 / Math.Sqrt(u) * Math.Log(u) }).Sum(k=>k.sum); return query; }
pero existe un método mejor y es que toda colección contiene el método Sum, y podremos entonces hacer lo siguiente
static double collectionSum() { return list.Sum(k => 1 / Math.Sqrt(k) * Math.Log(k)); }
Bien, todavía habría alguna otra forma de iterar sobre colecciones, pero con estas nos basta y ahora vamos a ver como se comportan en base a su rendimiento y para ello he creado cuatro hilos, de los cuales cada uno generará la suma de los elementos calculados de la colección, pero cada uno con su método y una vez que terminen todos, que nos muestre el tiempo invertido en los cuatro procesos y cual ha sido el más eficiente. Lanzaremos los procesos por este orden, foreach estándar, expresión foreach lambda, Linq sintáxis de consulta y por último la suma directa sobre la colección mediante expresión lambda.
static void inBackGround() { string text = ""; List cronos = new List(); Stopwatch stw = new Stopwatch(); Stopwatch stw1 = new Stopwatch(); Stopwatch stw2 = new Stopwatch(); Stopwatch stw3 = new Stopwatch(); Stopwatch stw4 = new Stopwatch(); cronos.Add(new KeyValuePair(stw1, "For Each Estandar")); cronos.Add(new KeyValuePair(stw2, "For Each Lambda")); cronos.Add(new KeyValuePair(stw2, "Query LINQ")); cronos.Add(new KeyValuePair(stw2, "Colección.Sum()")); stw.Start(); Task[] tasks; var t1 = Task.Factory.StartNew(() => { stw1.Start(); text = string.Format("For Each Estandar. n = {0}. Resultado = {1}. Tiempo del proceso: {2}", n, forEachStandar(), stw1.Elapsed); stw1.Stop(); Console.WriteLine(text); }); var t2 = Task.Factory.StartNew(() => { stw2.Start(); text = string.Format("For Each Lambda. n = {0}. Resultado = {1}. Tiempo del proceso: {2}", n, forEachLambda(), stw2.Elapsed); stw2.Stop(); Console.WriteLine(text); }); var t3 = Task.Factory.StartNew(() => { stw3.Start(); text = string.Format("Query LINQ. n = {0}. Resultado = {1}. Tiempo del proceso: {2}", n, queryLinq(), stw3.Elapsed); stw3.Stop(); Console.WriteLine(text); }); var t4 = Task.Factory.StartNew(() => { stw4.Start(); text = string.Format("Colección.Sum(). n = {0}. Resultado = {1}. Tiempo del proceso: {2}", n, collectionSum(), stw4.Elapsed); stw4.Stop(); Console.WriteLine(text); }); tasks = new Task[] { t1, t2, t3, t4 }; Task.WaitAll(tasks); Console.WriteLine(string.Format("Los cuatro procesos han terminado. Tiempo: {0}",stw.Elapsed)); var theBest = cronos.Where(k => k.Key.Elapsed == cronos.Min(m => m.Key.Elapsed)).FirstOrDefault(); Console.WriteLine(string.Format("El más efectivo ha sido {0} con un tiempo de {1}",theBest.Value, theBest.Key.Elapsed)); Console.Read(); }
y ahora el resultado en el que el ganador en cuanto a rendimiento es el método For Each Lambda
por muy poco sobre la suma sobre la colección, el foreach estándar y el menos eficiente la consulta Linq.
