2011-12-16 15 views
30

es mejor hacer:¿Es mejor declarar una variable dentro o fuera de un bucle?

variable1Type foo; 
variable2Type baa; 

foreach(var val in list) 
{ 
    foo = new Foo(...); 
    foo.x = FormatValue(val); 

    baa = new Baa(); 
    baa.main = foo; 
    baa.Do(); 
} 

O:

foreach(var val in list) 
{ 
    variable1Type foo = new Foo(...); 
    foo.x = FormatValue(val); 

    variable2Type baa = new Baa(); 
    baa.main = foo; 
    baa.Do(); 
} 

La pregunta es: ¿Qué es más rápido 1 o 2 casos caso? ¿La diferencia es insignificante? ¿Es lo mismo en aplicaciones reales? Esto puede ser una optimización-micro, pero realmente quiero saber cuál es mejor.

+1

No hay diferencia de rendimiento si no está capturando la variable en una lambda. – Jon

+3

Debe valorar la diferencia semántica y no la diferencia de rendimiento, que en este caso no es ninguna. Al declarar dentro del bucle automáticamente sabrá que solo se usa dentro del bucle. –

+0

Odio hacerlo, pero -1: su pregunta no indica ningún esfuerzo de investigación. Sin duda podrías haber probado esto por tu cuenta. –

Respuesta

46

En cuanto al rendimiento, vamos a tratar de ejemplos concretos:

public void Method1() 
{ 
    foreach(int i in Enumerable.Range(0, 10)) 
    { 
    int x = i * i; 
    StringBuilder sb = new StringBuilder(); 
    sb.Append(x); 
    Console.WriteLine(sb); 
    } 
} 
public void Method2() 
{ 
    int x; 
    StringBuilder sb; 
    foreach(int i in Enumerable.Range(0, 10)) 
    { 
    x = i * i; 
    sb = new StringBuilder(); 
    sb.Append(x); 
    Console.WriteLine(sb); 
    } 
} 

deliberadamente cogí tanto un tipo de valor y un tipo de referencia en el caso que afecta a las cosas. Ahora, la IL para ellos:

.method public hidebysig instance void Method1() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] int32 i, 
     [1] int32 x, 
     [2] class [mscorlib]System.Text.StringBuilder sb, 
     [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator) 
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10 
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32) 
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() 
    L_000d: stloc.3 
    L_000e: br.s L_002f 
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() 
    L_0016: stloc.0 
    L_0017: ldloc.0 
    L_0018: ldloc.0 
    L_0019: mul 
    L_001a: stloc.1 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() 
    L_0020: stloc.2 
    L_0021: ldloc.2 
    L_0022: ldloc.1 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) 
    L_0028: pop 
    L_0029: ldloc.2 
    L_002a: call void [mscorlib]System.Console::WriteLine(object) 
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() 
    L_0035: brtrue.s L_0010 
    L_0037: leave.s L_0043 
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042 
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043 
} 

.method public hidebysig instance void Method2() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] int32 x, 
     [1] class [mscorlib]System.Text.StringBuilder sb, 
     [2] int32 i, 
     [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator) 
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10 
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32) 
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() 
    L_000d: stloc.3 
    L_000e: br.s L_002f 
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() 
    L_0016: stloc.2 
    L_0017: ldloc.2 
    L_0018: ldloc.2 
    L_0019: mul 
    L_001a: stloc.0 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() 
    L_0020: stloc.1 
    L_0021: ldloc.1 
    L_0022: ldloc.0 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) 
    L_0028: pop 
    L_0029: ldloc.1 
    L_002a: call void [mscorlib]System.Console::WriteLine(object) 
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() 
    L_0035: brtrue.s L_0010 
    L_0037: leave.s L_0043 
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042 
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043 
} 

Como se puede ver, además de la orden en la pila del compilador pasó a elegir - lo que podría muy bien haber sido un orden diferente - que no tenía absolutamente ningún efecto. A su vez, realmente no hay nada que le esté dando la trepidación para hacer mucho uso de lo que el otro no le está dando.

Aparte de eso, hay un tipo de diferencia.

En mi Method1(), x y sb están en el ámbito de la foreach, y no se puede acceder ya sea deliberada o accidentalmente fuera de ella.

En mi Method2(), x y sb no se conocen en tiempo de compilación que se asignará un valor fiable dentro del foreach (el compilador no conoce la foreach llevará a cabo al menos un bucle), por lo que está prohibido el uso de ella .

Hasta ahora, no hay diferencia real.

me puedo sin embargo asignar y utilizar x y/o sb fuera del foreach. Como regla, diría que es probable que la mayoría de las veces este alcance sea pobre, por lo que preferiría Method1, pero podría tener alguna razón sensata para querer referirme a ellos (de manera más realista si no estuvieran posiblemente sin asignar), en qué caso iría por Method2.

Aún así, esa es una cuestión de cómo cada código se puede extender o no, no una diferencia del código tal como está escrito. Realmente, no hay diferencia.

+2

+1 para crear un caso de prueba y usarlo para proporcionar pruebas que respalden sus afirmaciones. Buen trabajo. –

+0

Su punto es generalmente válido, y también verificaría el IL si no estuviera usando Mac. Sin embargo, el código IL todavía depende del compilador, por lo tanto, afirmar que no habría diferencia en el nivel IL no es del todo correcto desde el punto de vista técnico (aunque yo diría que es 99.9% correcto). De todos modos, creo que el programador más sensato y experimentado elegirá escribir como caso 2 sin vacilar. – tia

+1

@tia, mostrando el IL es evidencia de que podría ser el mismo, y la prueba de que es lo mismo con las implementaciones actuales. No es una prueba de que * debería * ser el mismo, pero como el código C# es equivalente, cualquier diferencia sería una falla menor en el compilador (al producir código menos eficiente para el que fuera menos eficiente). Estas dos cosas juntas (que * deberían * ser las mismas, y que * es * lo mismo) completan la respuesta. –

3

No importa, no tiene ningún efecto sobre el rendimiento en absoluto.

pero realmente quiero saber hacer bien.

La mayoría le dirá dentro del circuito tiene más sentido.

3

Es solo una cuestión de alcance. En este caso, donde foo y baa solo se usan dentro del bucle for, es una buena práctica declararlos dentro del bucle. Es más seguro también.

0

Ambos son completamente válidos, no estoy seguro de que haya una "manera correcta" de hacerlo.

Su primer caso es más eficiente con la memoria (al menos a corto plazo). Declarar sus variables dentro del bucle obligará a una mayor reasignación de la memoria; sin embargo, con el recolector de basura de .NET, cuando esas variables se salgan del alcance, se limpiarán periódicamente, pero no necesariamente de inmediato. La diferencia de velocidad es posiblemente insignificante.

El segundo caso es de hecho un poco más seguro, ya que limitar el alcance de sus variables tanto como sea posible suele ser una buena práctica.

+0

Su descripción del GC es incorrecta.En cualquier caso, si ya no se usan variable1Type y variable2Type, los objetos a los que apuntan serán elegibles para la recopilación cuando el método retorna o cuando otra variable local usa su ranura en la pila se usa para otra variable. Los dos son idénticos en lo que respecta a GC. El alcance no tiene nada que ver con la recolección de basura. El alcance afecta lo que * puede * hacer con una variable, el compilador usará la memoria de la pila dependiendo de lo que * hiciste *, y el GC puede reclamar cualquier cosa sin una referencia en vivo. –

0

En JS, la asignación de memoria es completa cada vez, en C#, no existe tal diferencia, pero si la variable local es capturada por un método anónimo como la expresión lambda, será importante.

1

OK, respondí esto sin darme cuenta de dónde el póster original creaba un objeto nuevo cada vez que pasaba por el ciclo y no simplemente 'usando' el objeto. Entonces, no, no debería haber una diferencia insignificante cuando se trata de rendimiento. Con esto, iría con el segundo método y declararía el objeto dentro del ciclo. De esta forma, se limpiará durante el próximo pase de GC y mantendrá el objeto dentro del alcance.

--- Voy a dejar mi respuesta original, simplemente porque lo escribí todo, y podría ayudar a alguien más que busca este mensaje más adelante. Prometo que en el futuro prestaré más atención antes de intentar responder la siguiente pregunta.

Tim

realidad, creo que hay una diferencia. Si recuerdo correctamente, cada vez que cree un nuevo objeto = new foo() obtendrá ese objeto agregado al montón de memoria. Por lo tanto, al crear los objetos en el bucle, se agregarán a la sobrecarga del sistema. Si sabes que el ciclo va a ser pequeño, no es un problema.

Por lo tanto, si termina en un bucle con decir 1000 objetos en él, va a crear 1000 variables que no se eliminarán hasta la próxima recolección de basura. Ahora acceda a una base de datos donde desea hacer algo y tiene más de 20,000 filas para trabajar ... Puede crear una demanda de sistema bastante grande según el tipo de objeto que esté creando.

Esto debería ser fácil de probar ... Cree una aplicación que cree 10.000 elementos con una marca de tiempo cuando ingrese al bucle y cuando salga. La primera vez que lo haga, declare la variable antes del ciclo y la próxima vez durante el ciclo. Sin embargo, es posible que deba aumentar esa cuenta mucho más alta que 10K para ver una diferencia real en la velocidad.

Además, existe el problema de alcance. Si se crea en el ciclo, desaparecerá una vez que salga del ciclo, por lo que no podrá acceder a él nuevamente. Pero esto también ayuda con la limpieza ya que la recolección de basura eventualmente se deshará de ella una vez que salga del ciclo.

Tim

+0

Las variables no causan memoria en el montón, los objetos sí lo hacen. Están creando el mismo número de objetos cada vez. –

+0

¿Una variable no es un objeto? Pensé que una variable de cadena es un objeto Class system.string. Entonces cuando haces algo como "VariableType1 foo = new VariableType1();" ¿No estás creando eso como un objeto que debe colocarse en la memoria? –

+0

No. 'string x;' variable en la pila para el objeto a asignar, pero no objeto real en el montón. 'x = nueva cadena ('a', 23);' ahora hay un objeto en el montón. 'string a = x; cadena b = x; cadena c = x; string d = x; '4 variables más, pero solo un objeto en el montón. 'a = b = c = d = x = nueva cadena ('b', 2);' uno o dos objetos en el montón, ya que la primera cadena será GC en algún momento, pero no sabemos exactamente cuándo. En la pregunta, ambas formas llaman 'new Foo()' y 'new Baa()' el mismo número de veces. Es eso lo que usa memoria de montón. –

Cuestiones relacionadas