Declarar una variable dentro o fuera de un bucle foreach: ¿cuál es más rápido / mejor?

92

¿Cuál de estos es el más rápido / mejor?

Éste:

List<User> list = new List<User>();
User u;

foreach (string s in l)
{
    u = new User();
    u.Name = s;
    list.Add(u);
}

O este:

List<User> list = new List<User>();

foreach (string s in l)
{
    User u = new User();
    u.Name = s;
    list.Add(u);
}

Mis habilidades de desarrollo novato me dicen que la primera es mejor, pero un amigo me dice que estoy equivocado, pero que no puede darme una buena razón por la cual la segunda es mejor.

¿Existe alguna diferencia en el rendimiento?

Marcus
fuente

Respuestas:

112

En cuanto al rendimiento, ambos ejemplos se compilan en el mismo IL, por lo que no hay diferencia.

El segundo es mejor, porque expresa más claramente su intención si usolo se usa dentro del ciclo.

dtb
fuente
10
Tenga en cuenta que no es una diferencia si la variable es capturado por una expresión lambda o delegado anónimo; ver Trampa variable externa .
dtb
¿Puede explicar por qué ambos se compilan en el mismo IL? Estoy bastante seguro de que C # no eleva las declaraciones de variables hasta la parte superior de la función como lo hace JavaScript.
styfle
4
@styfle aquí está la respuesta a su pregunta.
David Sherret
Los siguientes enlaces de Stack Overflow proporcionan respuestas más detalladas: 1) Jon Hanna y 2) StriplingWarrior
user3613932
14

En cualquier caso, la mejor manera sería usar un constructor que tome un Nombre ... o, de lo contrario, explotar la notación de llaves:

foreach (string s in l)
{
    list.Add(new User(s));
}

o

foreach (string s in l)
{
    list.Add(new User() { Name = s });
}

o incluso mejor, LINQ:

var list = l.Select( s => new User { Name = s});

Ahora, mientras que su primer ejemplo podría, en algunos casos, ser imperceptiblemente más rápido, el segundo es mejor porque es más legible y el compilador puede descartar la variable (y omitirla por completo) ya que no se usa fuera del foreachalcance del '.

Tordek
fuente
6
Comentario necrofílico del día: "o mejor aún, LINQ". Seguro que es una línea de código, y eso nos hace sentir bien como desarrolladores. Pero la versión de cuatro líneas es MUCHO más comprensible y, por lo tanto, se puede mantener.
Oskar Austegard
5
Apenas. Con la versión LINQ, sé que lo que estoy haciendo es inmutable y funciona en todos los elementos.
Tordek
6

Una declaración no hace que se ejecute ningún código, por lo que no es un problema de rendimiento.

El segundo es lo que quieres decir, y es menos probable que cometas un error estúpido si lo haces de la segunda forma, así que úsala. Intente siempre declarar variables en el ámbito más pequeño necesario.

Y además, la mejor forma es usar Linq:

List<User> users = l.Select(name => new User{ Name = name }).ToList();
Mark Byers
fuente
2
Me encanta la línea "Intente siempre declarar variables en el ámbito más pequeño necesario". Creo que una sola línea puede responder muy bien a la pregunta.
Manjoor
5

Siempre que tenga una pregunta sobre el rendimiento, lo único que debe hacer es medir: ejecutar un ciclo alrededor de su prueba y cronometrarla.

Para responder a su pregunta, sin medir :-) o sin mirar el ilasma generado, ninguna diferencia se notaría en un número significativo de iteraciones y la operación más costosa en su código es probable que sea la asignación de usuarios por algunos pedidos. de magnitud, así que concéntrese en la claridad del código (como debería en general) y vaya con 2.

Oh, es tarde y supongo que solo intento decirte que no te preocupes por este tipo de cosas o te quedes atrapado en detalles como este.

K

Kevin Shea
fuente
gracias por la propina, creo que cronometraré algunas otras cosas por las que también he estado dudando, jeje: D
Marcus
Si desea profundizar en lo que afecta al rendimiento, considere usar un generador de perfiles de código. Al menos, comenzará a darle una idea de qué tipo de código y operaciones requieren más tiempo. ProfileSharp y EqatecProfilers son gratuitos y suficientes para comenzar.
Kevin Shea
1

El segundo es mejor. Quiere tener un nuevo usuario en cada iteración.

Jarrett Widman
fuente
1

Técnicamente, el primer ejemplo ahorrará algunos nanosegundos porque el marco de la pila no tendrá que moverse para asignar una nueva variable, pero esta es una cantidad tan pequeña de tiempo de CPU que no lo notará, eso es si el compilador no lo hace. optimizar cualquier diferencia de todos modos.

Erik Funkenbusch
fuente
Estoy bastante seguro de que CLR no asigna "una nueva variable" en cada iteración de un bucle.
dtb
Bueno, el compilador puede optimizar eso, pero el espacio de la pila debe asignarse para cualquier variable en un bucle. Esto dependería de la implementación y una implementación puede simplemente mantener el marco de la pila igual, mientras que otra (digamos Mono) podría liberar la pila y luego recrearla en cada ciclo.
Erik Funkenbusch
16
Todas las variables locales de un método (de nivel superior o anidadas en un bucle) se compilan en variables de nivel de método en IL. El espacio para las variables se asigna antes de que se ejecute el método, no cuando se llega a una rama con la declaración en C #.
dtb
1
@dtb ¿Tiene una fuente para esta afirmación?
styfle
1

En este escenario, la segunda versión es mejor.

En general, si solo necesita acceder al valor dentro del cuerpo de la iteración, elija la segunda versión. Por otro lado, si hay algún estado final, la variable se mantendrá más allá del cuerpo del ciclo, luego declare y luego use la primera versión.

csj
fuente
0

No debería haber una diferencia perceptible en el rendimiento.

Jacob Adams
fuente
0

Fui a verificar este problema. Sorprendentemente descubrí en mis pruebas sucias que la segunda opción es incluso un poco más rápida todo el tiempo.

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    void Method1()
    {
      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }
    }

    void Method2()
    {

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }
    }
  }

  public class User { public string Name; }
}

Verifiqué el CIL pero no es idéntico.

ingrese la descripción de la imagen aquí

Así que preparé algo para lo que quería que fuera una prueba mucho mejor.

namespace Test
{
  class Loop
  { 

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    public void Method1()
    {
      sw.Restart();

      C c;
      C c1;
      C c2;
      C c3;
      C c4;

      int i = 1000;
      while (i-- > 0)
      {
        c = new C();
        c1 = new C();
        c2 = new C();
        c3 = new C();
        c4 = new C();        
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    public void Method2()
    {
      sw.Restart();

      int i = 1000;
      while (i-- > 0)
      {
        var c = new C();
        var c1 = new C();
        var c2 = new C();
        var c3 = new C();
        var c4 = new C();
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  class C { }
}

También en este caso, el segundo método siempre ganaba, pero luego verifiqué que el CIL no encontró diferencias.

ingrese la descripción de la imagen aquí

No soy un gurú de la lectura de CIL, pero no veo ningún problema de declinación. Como ya se señaló, la declaración no es asignación, por lo que no hay una penalización de rendimiento en ella.

Prueba

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    void Method1()
    {
      sw.Restart();

      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    void Method2()
    {
      sw.Restart();

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  public class User { public string Name; }
Ucho
fuente