Diferencia de rendimiento para las estructuras de control ‘for’ y ‘foreach’ en C #

¿Qué fragmento de código proporcionará un mejor rendimiento? Los siguientes segmentos de código se escribieron en C #.

1.

for(int counter=0; counter<list.Count; counter++) { list[counter].DoSomething(); } 

2.

 foreach(MyType current in list) { current.DoSomething(); } 

Bueno, en parte depende del tipo exacto de list . También dependerá de la CLR exacta que estés usando.

Si es de alguna manera significativa o no dependerá de si está haciendo un verdadero trabajo en el circuito. En casi todos los casos, la diferencia con el rendimiento no será significativa, pero la diferencia con la legibilidad favorece al ciclo foreach .

Yo personalmente usaría LINQ para evitar el “si” también:

 foreach (var item in list.Where(condition)) { } 

EDITAR: Para aquellos de ustedes que afirman que iterar sobre una List con foreach produce el mismo código que el ciclo for , aquí hay evidencia de que no:

 static void IterateOverList(List list) { foreach (object o in list) { Console.WriteLine(o); } } 

Produce IL de:

 .method private hidebysig static void IterateOverList(class [mscorlib]System.Collections.Generic.List`1 list) cil managed { // Code size 49 (0x31) .maxstack 1 .locals init (object V_0, valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator V_1) IL_0000: ldarg.0 IL_0001: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator class [mscorlib]System.Collections.Generic.List`1::GetEnumerator() IL_0006: stloc.1 .try { IL_0007: br.s IL_0017 IL_0009: ldloca.s V_1 IL_000b: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::get_Current() IL_0010: stloc.0 IL_0011: ldloc.0 IL_0012: call void [mscorlib]System.Console::WriteLine(object) IL_0017: ldloca.s V_1 IL_0019: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::MoveNext() IL_001e: brtrue.s IL_0009 IL_0020: leave.s IL_0030 } // end .try finally { IL_0022: ldloca.s V_1 IL_0024: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator IL_002a: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_002f: endfinally } // end handler IL_0030: ret } // end of method Test::IterateOverList 

El comstackdor trata las matrices de forma diferente, convirtiendo un bucle foreach básicamente en un bucle for , pero no en List . Aquí está el código equivalente para una matriz:

 static void IterateOverArray(object[] array) { foreach (object o in array) { Console.WriteLine(o); } } // Compiles into... .method private hidebysig static void IterateOverArray(object[] 'array') cil managed { // Code size 27 (0x1b) .maxstack 2 .locals init (object V_0, object[] V_1, int32 V_2) IL_0000: ldarg.0 IL_0001: stloc.1 IL_0002: ldc.i4.0 IL_0003: stloc.2 IL_0004: br.s IL_0014 IL_0006: ldloc.1 IL_0007: ldloc.2 IL_0008: ldelem.ref IL_0009: stloc.0 IL_000a: ldloc.0 IL_000b: call void [mscorlib]System.Console::WriteLine(object) IL_0010: ldloc.2 IL_0011: ldc.i4.1 IL_0012: add IL_0013: stloc.2 IL_0014: ldloc.2 IL_0015: ldloc.1 IL_0016: ldlen IL_0017: conv.i4 IL_0018: blt.s IL_0006 IL_001a: ret } // end of method Test::IterateOverArray 

Curiosamente, no puedo encontrar esto documentado en la especificación C # 3 en cualquier lugar …

A for loop se comstack para codificar aproximadamente equivalente a esto:

 int tempCount = 0; while (tempCount < list.Count) { if (list[tempCount].value == value) { // Do something } tempCount++; } 

Donde como un bucle foreach se comstack para codificar aproximadamente equivalente a esto:

 using (IEnumerator e = list.GetEnumerator()) { while (e.MoveNext()) { T o = (MyClass)e.Current; if (row.value == value) { // Do something } } } 

Como puede ver, todo dependerá de cómo se implemente el enumerador y cómo se implemente el indexador de listas. Como resultado, el enumerador para tipos basados ​​en matrices normalmente se escribe de la siguiente manera:

 private static IEnumerable MyEnum(List list) { for (int i = 0; i < list.Count; i++) { yield return list[i]; } } 

Como puede ver, en este caso no hará mucha diferencia, sin embargo, el enumerador de una lista vinculada probablemente se vería así:

 private static IEnumerable MyEnum(LinkedList list) { LinkedListNode current = list.First; do { yield return current.Value; current = current.Next; } while (current != null); } 

En .NET , encontrará que la clase LinkedList ni siquiera tiene un indexador, por lo que no podría hacer su bucle for en una lista vinculada; pero si pudiera, el indexador debería escribirse así:

 public T this[int index] { LinkedListNode current = this.First; for (int i = 1; i <= index; i++) { current = current.Next; } return current.value; } 

Como puede ver, llamar a esto varias veces en un bucle va a ser mucho más lento que usar un enumerador que pueda recordar dónde está en la lista.

Una prueba fácil de semivardar. Hice una pequeña prueba, solo para ver. Aquí está el código:

 static void Main(string[] args) { List intList = new List(); for (int i = 0; i < 10000000; i++) { intList.Add(i); } DateTime timeStarted = DateTime.Now; for (int i = 0; i < intList.Count; i++) { int foo = intList[i] * 2; if (foo % 2 == 0) { } } TimeSpan finished = DateTime.Now - timeStarted; Console.WriteLine(finished.TotalMilliseconds.ToString()); Console.Read(); } 

Y aquí está la sección foreach:

 foreach (int i in intList) { int foo = i * 2; if (foo % 2 == 0) { } } 

Cuando reemplacé el para con un foreach - el foreach era 20 milisegundos más rápido - consistentemente . El for era 135-139ms mientras que el foreach era 113-119ms. Cambié de un lado a otro varias veces, asegurándome de que no se tratara de un proceso que acaba de iniciarse.

Sin embargo, cuando eliminé el foo y el enunciado if, el for era más rápido en 30 ms (foreach era 88 ms y para 59 ms). Ambos eran caparazones vacíos. Supongo que el foreach en realidad pasó una variable donde el símbolo simplemente incrementaba una variable. Si agregué

 int foo = intList[i]; 

Entonces el para se vuelve lento en unos 30 ms. Supongo que esto tiene que ver con crear foo y agarrar la variable en la matriz y asignarla a foo. Si solo accede a intList [i], entonces no tiene esa penalización.

Honestamente. Esperaba que el foreach fuera un poco más lento en todas las circunstancias, pero no lo suficiente como para importar en la mayoría de las aplicaciones.

editar: aquí está el nuevo código que utiliza sugerencias de Jons (134217728 es la int más grande que puede tener antes de que se produzca la excepción System.OutOfMemory):

 static void Main(string[] args) { List intList = new List(); Console.WriteLine("Generating data."); for (int i = 0; i < 134217728 ; i++) { intList.Add(i); } Console.Write("Calculating for loop:\t\t"); Stopwatch time = new Stopwatch(); time.Start(); for (int i = 0; i < intList.Count; i++) { int foo = intList[i] * 2; if (foo % 2 == 0) { } } time.Stop(); Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms"); Console.Write("Calculating foreach loop:\t"); time.Reset(); time.Start(); foreach (int i in intList) { int foo = i * 2; if (foo % 2 == 0) { } } time.Stop(); Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms"); Console.Read(); } 

Y aquí están los resultados:

Generando datos. Cálculo de bucle: 2458 ms Cálculo de bucle foreach: 2005ms

Intercambiarlos para ver si se trata del orden de las cosas produce los mismos resultados (casi).

Nota: esta respuesta se aplica más a Java que a C #, ya que C # no tiene un indexador en LinkedLists , pero creo que el punto general aún se cumple.

Si la list que está trabajando es LinkedList , el rendimiento del código del indexador (acceso de estilo de matriz ) es mucho peor que el uso del IEnumerator del foreach , para listas grandes.

Cuando acceda al elemento 10.000 en una LinkedList utilizando la syntax del indexador: list[10000] , la lista vinculada comenzará en el nodo principal, y recorrerá el Next puntero diez mil veces, hasta que scope el objeto correcto. Obviamente, si haces esto en un bucle, obtendrás:

 list[0]; // head list[1]; // head.Next list[2]; // head.Next.Next // etc. 

Cuando llama a GetEnumerator (implícitamente usando forach -syntax), obtendrá un objeto IEnumerator que tiene un puntero al nodo principal. Cada vez que llamas a MoveNext , ese puntero se mueve al siguiente nodo, así:

 IEnumerator em = list.GetEnumerator(); // Current points at head em.MoveNext(); // Update Current to .Next em.MoveNext(); // Update Current to .Next em.MoveNext(); // Update Current to .Next // etc. 

Como puede ver, en el caso de LinkedList s, el método del indexador de matriz se vuelve más lento y más lento, cuanto más tiempo bucle (tiene que pasar por el mismo puntero una y otra vez). Mientras que IEnumerable solo opera en tiempo constante.

Por supuesto, como Jon dijo que esto realmente depende del tipo de list , si la list no es una LinkedList , sino una matriz, el comportamiento es completamente diferente.

Como otras personas han mencionado, aunque el rendimiento en realidad no importa mucho, el foreach siempre será un poco más lento debido al uso de IEnumerable / IEnumerator en el ciclo. El comstackdor traduce la construcción en llamadas en esa interfaz y para cada paso una función + una propiedad se llama en la construcción foreach.

 IEnumerator iterator = ((IEnumerable)list).GetEnumerator(); while (iterator.MoveNext()) { var item = iterator.Current; // do stuff } 

Esta es la expansión equivalente de la construcción en C #. Puede imaginar cómo el impacto en el rendimiento puede variar en función de las implementaciones de MoveNext y Current. Mientras que en un acceso de matriz, no tiene esas dependencias.

Después de leer suficientes argumentos de que “el ciclo foreach debería ser preferido para la legibilidad”, puedo decir que mi primera reacción fue “¿qué?” La legibilidad, en general, es subjetiva y, en este caso particular, aún más. Para alguien con experiencia en progtwigción (prácticamente, cada idioma antes de Java), los bucles son mucho más fáciles de leer que los bucles foreach. Además, las mismas personas que afirman que los bucles foreach son más legibles, también son partidarios de linq y otras “características” que hacen que el código sea difícil de leer y mantener, algo que demuestra el punto anterior.

Sobre el impacto en el rendimiento, vea la respuesta a esta pregunta.

EDITAR: hay colecciones en C # (como HashSet) que no tienen indexador. En estas colecciones, foreach es la única manera de iterar y es el único caso en el que creo que debería utilizarse.

Hay otro hecho interesante que se puede perder fácilmente al probar la velocidad de ambos bucles: Usar el modo de depuración no permite que el comstackdor optimice el código usando la configuración predeterminada.

Esto me llevó al interesante resultado de que el foreach es más rápido que en el modo de depuración. Mientras que el fort es más rápido que foreach en el modo de lanzamiento. Obviamente, el comstackdor tiene mejores formas de optimizar un bucle for que un bucle foreach que compromete varias llamadas a métodos. Un bucle for es, por cierto, tan fundamental que es posible que incluso la CPU lo optimice.

En el ejemplo que proporcionó, definitivamente es mejor usar un bucle foreach lugar de un bucle for .

El constructo foreach estándar puede ser más rápido (1,5 ciclos por paso) que un for-loop (2 ciclos por paso), a menos que el bucle se haya desenrollado (1,0 ciclos por paso).

Entonces, para el código de todos los días, el rendimiento no es una razón para usar las construcciones más complejas for , while o do-while .

Echa un vistazo a este enlace: http://www.codeproject.com/Articles/146797/ Fast-and-Less- Fast-Loops-in- C


 ╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗ ║ Method ║ List ║ int[] ║ Ilist onList ║ Ilist on int[] ║ ╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣ ║ Time (ms) ║ 23,80 ║ 17,56 ║ 92,33 ║ 86,90 ║ ║ Transfer rate (GB/s) ║ 2,82 ║ 3,82 ║ 0,73 ║ 0,77 ║ ║ % Max ║ 25,2% ║ 34,1% ║ 6,5% ║ 6,9% ║ ║ Cycles / read ║ 3,97 ║ 2,93 ║ 15,41 ║ 14,50 ║ ║ Reads / iteration ║ 16 ║ 16 ║ 16 ║ 16 ║ ║ Cycles / iteration ║ 63,5 ║ 46,9 ║ 246,5 ║ 232,0 ║ ╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝