Pregunta

Siempre me he preguntado si, en general, declarar una variable desechable antes de un bucle, en lugar de repetidamente dentro del bucle, ¿hace alguna diferencia (rendimiento)? Un ejemplo de (bastante inútil) en Java:

a) declaración antes del bucle:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

b) declaración (repetidamente) dentro del bucle:

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

¿Cuál es mejor, a o b ?

Sospecho que la declaración de variable repetida (ejemplo b ) crea más sobrecarga en teoría , pero los compiladores son lo suficientemente inteligentes como para que no importe. El ejemplo b tiene la ventaja de ser más compacto y limitar el alcance de la variable a donde se usa. Sin embargo, tiendo a codificar según el ejemplo a .

Editar: Estoy especialmente interesado en el caso de Java.

¿Fue útil?

Solución

¿Qué es mejor, a o b ?

Desde una perspectiva de rendimiento, tendrías que medirlo. (Y en mi opinión, si puedes medir una diferencia, el compilador no es muy bueno).

Desde una perspectiva de mantenimiento, b es mejor. Declare e inicialice las variables en el mismo lugar, en el ámbito más estrecho posible. No deje un hueco entre la declaración y la inicialización, y no contamine los espacios de nombres que no necesita.

Otros consejos

Bueno, ejecuté tus ejemplos A y B 20 veces cada uno, repitiendo 100 millones de veces (JVM - 1.5.0)

A: tiempo de ejecución promedio: .074 seg

B: tiempo de ejecución promedio: .067 seg

Para mi sorpresa, B fue un poco más rápido. Tan rápido como las computadoras son ahora difíciles de decir si pudiera medir esto con precisión. Yo también lo codificaría de la forma A, pero diría que realmente no importa.

Depende del idioma y del uso exacto. Por ejemplo, en C # 1 no hizo ninguna diferencia. En C # 2, si la variable local es capturada por un método anónimo (o la expresión lambda en C # 3) puede hacer una diferencia muy significativa.

Ejemplo:

using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        int outer;
        for (int i=0; i < 10; i++)
        {
            outer = i;
            int inner = i;
            actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
        }

        foreach (Action action in actions)
        {
            action();
        }
    }
}

Salida:

Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9

La diferencia es que todas las acciones capturan la misma variable external , pero cada una tiene su propia variable separada inner .

Lo siguiente es lo que escribí y compilé en .NET.

double r0;
for (int i = 0; i < 1000; i++) {
    r0 = i*i;
    Console.WriteLine(r0);
}

for (int j = 0; j < 1000; j++) {
    double r1 = j*j;
    Console.WriteLine(r1);
}

Esto es lo que recibo de .NET Reflector cuando CIL se devuelve al código.

for (int i = 0; i < 0x3e8; i++)
{
    double r0 = i * i;
    Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
    double r1 = j * j;
    Console.WriteLine(r1);
}

Así que ambos se ven exactamente iguales después de la compilación. En los idiomas administrados, el código se convierte en código CL / byte y en el momento de la ejecución se convierte en lenguaje de máquina. Así que en el lenguaje de máquina un doble no puede ser creado en la pila Puede que solo sea un registro, ya que el código refleja que es una variable temporal para la función WriteLine . Hay un conjunto de reglas de optimización solo para bucles. Así que el hombre promedio no debería preocuparse por eso, especialmente en los idiomas administrados. Hay casos en los que puede optimizar el código de administración, por ejemplo, si tiene que concatenar un gran número de cadenas utilizando solo la cadena a ; a + = anotherstring [i] vs utilizando StringBuilder . Hay una gran diferencia en el rendimiento entre ambos. Hay muchos de estos casos en los que el compilador no puede optimizar su código, ya que no puede entender qué se pretende en un ámbito más amplio. Pero puede optimizar bastante las cosas básicas para ti.

Esto es un gotcha en VB.NET. El resultado de Visual Basic no reinicializará la variable en este ejemplo:

For i as Integer = 1 to 100
    Dim j as Integer
    Console.WriteLine(j)
    j = i
Next

' Output: 0 1 2 3 4...

Esto imprimirá 0 la primera vez (¡las variables de Visual Basic tienen valores predeterminados cuando se declaran!) pero i cada vez después de eso.

Si agrega un = 0 , obtendrá lo que podría esperar:

For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...

Hice una prueba simple:

int b;
for (int i = 0; i < 10; i++) {
    b = i;
}

vs

for (int i = 0; i < 10; i++) {
    int b = i;
}

Compilé estos códigos con gcc - 5.2.0. Y luego desmonté el principal () de estos dos códigos y ese es el resultado:

1 & # 186 ;:

   0x00000000004004b6 <+0>:     push   rbp
   0x00000000004004b7 <+1>:     mov    rbp,rsp
   0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret

vs

2 & # 186;

   0x00000000004004b6 <+0>: push   rbp
   0x00000000004004b7 <+1>: mov    rbp,rsp
   0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret 

Que son exactamente el mismo resultado asm. ¿No es una prueba de que los dos códigos producen lo mismo?

Siempre usaría A (en lugar de confiar en el compilador) y también podría volver a escribir para:

for(int i=0, double intermediateResult=0; i<1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

Esto todavía restringe middleResultado al alcance del bucle, pero no se vuelve a declarar durante cada iteración.

Es dependiente del idioma: IIRC C # optimiza esto, así que no hay ninguna diferencia, pero JavaScript (por ejemplo) hará la asignación de memoria completa cada vez.

En mi opinión, b es la mejor estructura. En a, el último valor de intermediosResultado se mantiene una vez finalizado el bucle.

Editar: Esto no hace mucha diferencia con los tipos de valor, pero los tipos de referencia pueden ser algo pesados. Personalmente, me gusta que las variables se eliminen de referencia tan pronto como sea posible para la limpieza, yb lo hace por usted,

Sospecho que algunos compiladores podrían optimizar ambos para ser el mismo código, pero ciertamente no todos. Así que diría que estás mejor con el primero. La única razón para esto último es si desea asegurarse de que la variable declarada se use solo dentro de su bucle.

Como regla general, declaro mis variables en el ámbito más interno posible. Entonces, si no estás utilizando intermediosResultado fuera del bucle, entonces me quedo con B.

Un compañero de trabajo prefiere la primera forma, diciéndole que es una optimización, que prefiere reutilizar una declaración.

Prefiero la segunda (y trato de persuadir a mi compañero de trabajo! ;-)), habiendo leído eso:

  • Reduce el alcance de las variables a donde se necesitan, lo que es bueno.
  • Java optimiza lo suficiente como para no hacer una diferencia significativa en el rendimiento. IIRC, quizás la segunda forma sea aún más rápida.

De todos modos, cae en la categoría de optimización prematura que se basa en la calidad del compilador y / o JVM.

Hay una diferencia en C # si estás usando la variable en un lambda, etc. Pero en general el compilador básicamente hará lo mismo, asumiendo que la variable solo se usa dentro del bucle.

Dado que son básicamente lo mismo: tenga en cuenta que la versión b hace que sea mucho más obvio para los lectores que la variable no es, y no puede, usarse después del bucle. Además, la versión b es mucho más fácil de refaccionar. Es más difícil extraer el cuerpo del bucle en su propio método en la versión a. Además, la versión b le asegura que no hay ningún efecto secundario en dicha refactorización.

Por lo tanto, la versión a me molesta hasta el final, porque no tiene ningún beneficio y hace que sea mucho más difícil razonar sobre el código ...

Bueno, siempre puedes hacer un alcance para eso:

{ //Or if(true) if the language doesn't support making scopes like this
    double intermediateResult;
    for (int i=0; i<1000; i++) {
        intermediateResult = i;
        System.out.println(intermediateResult);
    }
}

De esta manera, solo declaras la variable una vez, y morirá cuando dejes el bucle.

Siempre he pensado que si declara sus variables dentro de su bucle, está desperdiciando memoria. Si tienes algo como esto:

for(;;) {
  Object o = new Object();
}

Entonces, no solo es necesario crear el objeto para cada iteración, sino que debe haber una nueva referencia asignada para cada objeto. Parece que si el recolector de basura es lento, entonces tendrás un montón de referencias pendientes que deben limpiarse.

Sin embargo, si tienes esto:

Object o;
for(;;) {
  o = new Object();
}

Entonces solo estás creando una sola referencia y asignándole un nuevo objeto cada vez. Claro, puede tardar un poco más en salir del alcance, pero solo hay una referencia pendiente con la que lidiar.

Creo que depende del compilador y es difícil dar una respuesta general.

Mi práctica es la siguiente:

  • si el tipo de variable es simple (int, double, ...) prefiero la variante b (adentro).
    Motivo: reduciendo el alcance de la variable.

  • si el tipo de variable no es simple (algún tipo de clase o struct ) prefiero la variante a (afuera).
    Motivo: reduciendo el número de llamadas de ctor-dtor.

Desde una perspectiva de rendimiento, fuera es (mucho) mejor.

public static void outside() {
    double intermediateResult;
    for(int i=0; i < Integer.MAX_VALUE; i++){
        intermediateResult = i;
    }
}

public static void inside() {
    for(int i=0; i < Integer.MAX_VALUE; i++){
        double intermediateResult = i;
    }
}

Ejecuté ambas funciones 1 billón de veces cada una. Fuera () tomó 65 milisegundos. inside () tomó 1.5 segundos.

A) es una apuesta segura que B) ......... Imagina que si inicias una estructura en bucle en lugar de 'int' o 'float', ¿qué?

me gusta

typedef struct loop_example{

JXTZ hi; // where JXTZ could be another type...say closed source lib 
         // you include in Makefile

}loop_example_struct;

//then....

int j = 0; // declare here or face c99 error if in loop - depends on compiler setting

for ( ;j++; )
{
   loop_example loop_object; // guess the result in memory heap?
}

¡Seguro que estás obligado a enfrentar problemas con fugas de memoria! Por lo tanto, creo que 'A' es una apuesta más segura, mientras que 'B' es vulnerable a la acumulación de memoria, especialmente al trabajar con bibliotecas de código fuente cercanas.

Es una pregunta interesante. Según mi experiencia, hay una pregunta fundamental a considerar cuando se debate este asunto por un código:

¿Hay alguna razón por la que la variable deba ser global?

Tiene sentido declarar la variable solo una vez, globalmente, a diferencia de muchas veces localmente, porque es mejor para organizar el código y requiere menos líneas de código. Sin embargo, si solo fuera necesario declararlo localmente dentro de un método, lo inicializaría en ese método para que quede claro que la variable es exclusivamente relevante para ese método. Tenga cuidado de no llamar a esta variable fuera del método en el que se inicializa si elige la última opción; su código no sabrá de lo que está hablando e informará un error.

Además, como nota al margen, no duplique nombres de variables locales entre diferentes métodos, incluso si sus propósitos son casi idénticos; simplemente se vuelve confuso.

He probado para JS con Node 4.0.0 si alguien está interesado. La declaración fuera del bucle resultó en una mejora de rendimiento de ~ .5 ms en promedio en más de 1000 intentos con 100 millones de iteraciones de bucle por ensayo. Así que diré, adelante, y escríbalo de la manera más legible / mantenible que es B, imo. Yo pondría mi código en un violín, pero usé el módulo Nodo de rendimiento ahora. Aquí está el código:

var now = require("../node_modules/performance-now")

// declare vars inside loop
function varInside(){
    for(var i = 0; i < 100000000; i++){
        var temp = i;
        var temp2 = i + 1;
        var temp3 = i + 2;
    }
}

// declare vars outside loop
function varOutside(){
    var temp;
    var temp2;
    var temp3;
    for(var i = 0; i < 100000000; i++){
        temp = i
        temp2 = i + 1
        temp3 = i + 2
    }
}

// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;

// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varInside()
    var end = now()
    insideAvg = (insideAvg + (end-start)) / 2
}

// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varOutside()
    var end = now()
    outsideAvg = (outsideAvg + (end-start)) / 2
}

console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)

esta es la mejor forma

double intermediateResult;
int i = byte.MinValue;

for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1) de esta manera se declara una vez, tanto la variable, como no cada una para el ciclo. 2) la asignación es más amplia que cualquier otra opción. 3) Entonces, la regla de la mejor práctica es cualquier declaración fuera de la iteración para.

Probé lo mismo en Go, y comparé la salida del compilador usando go tool compile -S con go 1.9.4

Diferencia cero, según la salida del ensamblador.

Tuve esta misma pregunta durante mucho tiempo. Así que probé un código aún más simple.

Conclusión: Para tales casos hay NO diferencia de rendimiento.

Caso de bucle externo

int intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i+2;
    System.out.println(intermediateResult);
}

Caso de bucle interno

for(int i=0; i < 1000; i++){
    int intermediateResult = i+2;
    System.out.println(intermediateResult);
}

Revisé el archivo compilado en el descompilador de IntelliJ y, en ambos casos, obtuve el mismo Test.class

for(int i = 0; i < 1000; ++i) {
    int intermediateResult = i + 2;
    System.out.println(intermediateResult);
}

También desmonté el código para el caso utilizando el método que se describe en esta respuesta . Mostraré solo las partes relevantes a la respuesta

Caso de bucle externo

Code:
  stack=2, locals=3, args_size=1
     0: iconst_0
     1: istore_2
     2: iload_2
     3: sipush        1000
     6: if_icmpge     26
     9: iload_2
    10: iconst_2
    11: iadd
    12: istore_1
    13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    16: iload_1
    17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
    20: iinc          2, 1
    23: goto          2
    26: return
LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13      13     1 intermediateResult   I
            2      24     2     i   I
            0      27     0  args   [Ljava/lang/String;

Caso de bucle interno

Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     26
         9: iload_1
        10: iconst_2
        11: iadd
        12: istore_2
        13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        20: iinc          1, 1
        23: goto          2
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       7     2 intermediateResult   I
            2      24     1     i   I
            0      27     0  args   [Ljava/lang/String;

Si presta mucha atención, solo el Slot asignado a i y intermediateResult en LocalVariableTable se intercambia como Un producto de su orden de aparición. La misma diferencia en la ranura se refleja en otras líneas de código.

  • No se está realizando ninguna operación adicional
  • intermediateResult sigue siendo una variable local en ambos casos, por lo que no hay diferencia en el tiempo de acceso.

BONIFICACIÓN

Los compiladores hacen una tonelada de optimización, eche un vistazo a lo que sucede en este caso.

Caso de trabajo cero

for(int i=0; i < 1000; i++){
    int intermediateResult = i;
    System.out.println(intermediateResult);
}

Trabajo cero descompilado

for(int i = 0; i < 1000; ++i) {
    System.out.println(i);
}

Incluso si sé que mi compilador es lo suficientemente inteligente, no me gustaría confiar en él, y usaré la variante a).

La variante b) tiene sentido para mí solo si necesita desesperadamente que middleResult no esté disponible después del cuerpo del bucle. Pero no puedo imaginar una situación tan desesperada, de todos modos ...

EDIT: Jon Skeet hizo un muy buen punto, mostrando que la declaración de variables dentro de un bucle puede hacer una diferencia semántica real.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top