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

105

¿Qué fragmento de código ofrecerá 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();
}
Kthevar
fuente
31
Me imagino que realmente no importa. Si tiene problemas de rendimiento, es casi seguro que no se deba a esto. No es que no
debas
2
A menos que su aplicación sea muy crítica para el rendimiento, no me preocuparía por esto. Es mucho mejor tener un código limpio y fácilmente comprensible.
Fortyrunner
2
Me preocupa que algunas de las respuestas aquí parecen haber sido publicadas por personas que simplemente no tienen el concepto de iterador en ninguna parte de su cerebro y, por lo tanto, no tienen el concepto de enumeradores o punteros.
Ed James
3
Ese segundo código no se compilará. System.Object no tiene un miembro llamado 'valor' (a menos que sea realmente malvado, lo haya definido como un método de extensión y esté comparando delegados). Escriba fuertemente su foreach.
Trillian
1
El primer código tampoco se compilará, a menos que el tipo de listrealmente tenga un countmiembro en lugar de Count.
Jon Skeet

Respuestas:

130

Bueno, depende en parte del tipo exacto de list. También dependerá del CLR exacto que esté utilizando.

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

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 un List<T>con foreachproduce el mismo código que el forbucle, aquí hay evidencia de que no es así:

static void IterateOverList(List<object> 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<object> list) cil managed
{
  // Code size       49 (0x31)
  .maxstack  1
  .locals init (object V_0,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::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<object>::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<object>::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<object>
    IL_002a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_002f:  endfinally
  }  // end handler
  IL_0030:  ret
} // end of method Test::IterateOverList

El compilador trata las matrices de manera diferente, convirtiendo un foreachbucle básicamente en un forbucle, pero no List<T>. 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 ninguna parte ...

Jon Skeet
fuente
Jon, el escenario con List <T> arriba ... ¿se aplica eso también a otras colecciones? Además, ¿cómo supo esto (sin intención alguna de malicia) ... como en ... literalmente se tropezó con esto mientras intentaba responder a esta pregunta, anteriormente hace algún tiempo? Es tan ... aleatorio / secreto :)
Pure.Krome
5
He estado al tanto de las optimizaciones de matrices por un tiempo - las matrices son un tipo de colección "central"; el compilador de C # ya los conoce en profundidad, por lo que tiene sentido tratarlos de manera diferente. El compilador no tiene (y no debería) tener ningún conocimiento especial de List<T>.
Jon Skeet
Saludos :) y sí ... los arreglos fueron el primer concepto de colección que me enseñaron hace años y años en la universidad ... así que tendría sentido que el compilador es lo suficientemente inteligente como para lidiar con uno de (si no el) tipo más primitivo de colección. saludos de nuevo!
Pure.Krome
3
@JonSkeet La optimización del iterador de lista cambia el comportamiento cuando la lista se modifica durante la iteración. Pierde la excepción si se modifica. Todavía es posible optimizar, pero requiere verificar que no ocurran modificaciones (incluso en otros subprocesos, supongo).
Craig Gidney
5
@VeeKeyBee: Eso dijo Microsoft en 2004. a) las cosas cambian; b) el trabajo tendría que hacer pequeñas cantidades de trabajo en cada iteración para que esto sea significativo. Tenga foreachen cuenta que sobre una matriz es equivalente a de fortodos modos. Siempre codifique primero para la legibilidad, luego solo micro-optimice cuando tenga evidencia de que brinda un beneficio de rendimiento medible.
Jon Skeet
15

Un forbucle se compila en un código aproximadamente equivalente a esto:

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

Donde un foreachbucle se compila en un código aproximadamente equivalente a esto:

using (IEnumerator<T> 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 versus cómo se implemente el indexador de listas. Como resultado, el enumerador para tipos basados ​​en matrices normalmente se escribe de esta manera:

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

Entonces, 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<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

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

public T this[int index]
{
       LinkedListNode<T> 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 será mucho más lento que usar un enumerador que pueda recordar dónde está en la lista.

Martin Brown
fuente
12

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

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    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 de foreach:

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

Cuando reemplacé el for con un foreach, el foreach fue 20 milisegundos más rápido, consistentemente . El for fue de 135-139ms mientras que el foreach fue de 113-119ms. Cambié de un lado a otro varias veces, asegurándome de que no fuera un proceso que simplemente se inició.

Sin embargo, cuando eliminé el foo y la instrucción if, el for fue más rápido en 30 ms (foreach fue de 88 ms y for fue de 59 ms). Ambos eran cascos vacíos. Supongo que el foreach realmente pasó una variable donde el for solo estaba incrementando una variable. Si agregué

int foo = intList[i];

Luego, el for se vuelve lento en unos 30 ms. Supongo que esto tuvo que ver con la creación de foo y tomando la variable en la matriz y asignándola a foo. Si solo accede a intList [i], entonces no tiene esa penalización.

Honestamente ... esperaba que 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 usando las sugerencias de Jons (134217728 es el mayor int que puede tener antes de que se lance la excepción System.OutOfMemory):

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    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 for: 2458ms Cálculo de bucle foreach: 2005ms

Al intercambiarlos para ver si se trata del orden de las cosas, se obtienen los mismos resultados (casi).

Kenny Mann
fuente
6
Es mejor usar Cronómetro que DateTime. Ahora, y para ser honesto, no confiaría en ninguna carrera tan rápida.
Jon Skeet
8
Sus bucles foreach se ejecutan más rápido porque un 'for' evalúa la condición en cada iteración. En el caso de su ejemplo, esto genera una llamada de método adicional (para obtener list.count) En resumen, está comparando dos piezas de código diferentes, de ahí sus resultados extraños. Prueba 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'y el ciclo' for 'siempre se ejecutará más rápido, como se esperaba.
AR
1
Después de la compilación, for y foreach optimizan exactamente lo mismo cuando se trabaja con primitivas. No es hasta que presenta List <T> que difieren (enormemente) en velocidad.
Anthony Russell
9

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

Si el código listcon el que está trabajando es a LinkedList, el rendimiento del código indexador ( acceso al estilo de matriz ) es mucho peor que el de usar el IEnumeratorde foreach, para listas grandes.

Cuando accede al elemento 10.000 en a LinkedListusando la sintaxis del indexador:, list[10000]la lista enlazada comenzará en el nodo principal y atravesará el Nextpuntero diez mil veces, hasta que alcance 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 GetEnumerator(implícitamente usando la forach-sintaxis), obtendrá un IEnumeratorobjeto que tiene un puntero al nodo principal. Cada vez que llama 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 LinkedLists, el método del indexador de matriz se vuelve más y más lento, cuanto más largo es el bucle (tiene que pasar por el mismo puntero principal una y otra vez). Mientras que el IEnumerablejusto opera en tiempo constante.

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

Tom Lokhorst
fuente
4
LinkedList en .NET no tiene un indexador, por lo que en realidad no es una opción.
Jon Skeet
Oh, bueno, eso resuelve ese problema, entonces :-) Solo estoy revisando los LinkedList<T>documentos en MSDN, y tiene una API bastante decente. Lo más importante es que no tiene un get(int index)método, como lo tiene Java. Aún así, supongo que el punto sigue siendo válido para cualquier otra estructura de datos similar a una lista que exponga un indexador que es más lento que uno específico IEnumerator.
Tom Lokhorst
2

Como han mencionado otras personas, aunque el rendimiento en realidad no importa mucho, el foreach siempre será un poco más lento debido al uso de IEnumerable/ IEnumeratoren el ciclo. El compilador traduce la construcción en llamadas en esa interfaz y para cada paso se llama a una función + una propiedad 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 imaginarse cómo puede variar el impacto en el rendimiento según las implementaciones de MoveNext y Current. Mientras que en un acceso a una matriz, no tiene esas dependencias.

Charles Prakash Dasari
fuente
4
No olvide que hay una diferencia entre un acceso a una matriz y un acceso a un indexador. Si la lista está List<T>aquí, todavía existe el acierto (posiblemente en línea) de llamar al indexador. No es como si fuera un acceso a una matriz de metal desnudo.
Jon Skeet
¡Muy cierto! Es otra ejecución de propiedad y estamos a merced de la implementación.
Charles Prakash Dasari
1

Después de leer suficientes argumentos de que "el bucle foreach debería ser preferido por 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 programación (prácticamente, todos los lenguajes antes de Java), los bucles for 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 prueba el punto anterior.

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

EDITAR: Hay colecciones en C # (como el HashSet) que no tienen indexador. En estas colecciones, foreach es la única manera de iterar y es el único caso creo que se debe utilizar más de .

ThunderGr
fuente
0

Hay otro hecho interesante que puede pasarse por alto fácilmente al probar la velocidad de ambos bucles: el uso del modo de depuración no permite que el compilador optimice el código con la configuración predeterminada.

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

Sam
fuente
0

En el ejemplo que proporcionó, definitivamente es mejor usar un foreachbucle en lugar de un forbucle.

La foreachconstrucción estándar puede ser más rápida (1,5 ciclos por paso) que una simple for-loop(2 ciclos por paso), a menos que el bucle se haya desenrollado (1,0 ciclos por paso).

Así que para el código de todos los días, el rendimiento no es una razón para utilizar los más complejos for, whileo do-whileconstrucciones.

Consulte este enlace: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C


╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
        Method         List<int>  int[]  Ilist<int> onList<Int>  Ilist<int> 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               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

rpax
fuente
4
Puede volver a leer el artículo del proyecto de código que vinculó. Es un artículo interesante, pero dice exactamente lo contrario de tu publicación. Además, la tabla que ha recreado mide el rendimiento de acceder a una matriz y una Lista directamente, o mediante sus interfaces IList. Tampoco tengo nada que ver con la pregunta. :)
Paul Walls
0

puede leer sobre esto en Deep .NET - parte 1 Iteración

cubre los resultados (sin la primera inicialización) desde el código fuente .NET hasta el desmontaje.

por ejemplo - Iteración de matriz con un bucle foreach: ingrese la descripción de la imagen aquí

y - lista de iteraciones con foreach loop: ingrese la descripción de la imagen aquí

y los resultados finales: ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

O Yaacov
fuente