Declarando variables dentro o fuera de un ciclo

236

¿Por qué funciona bien lo siguiente?

String str;
while (condition) {
    str = calculateStr();
    .....
}

Pero se dice que este es peligroso / incorrecto:

while (condition) {
    String str = calculateStr();
    .....
}

¿Es necesario declarar variables fuera del ciclo?

Harry Joy
fuente

Respuestas:

289

El alcance de las variables locales siempre debe ser lo más pequeño posible.

En su ejemplo, supongo strque no se usa fuera del whilebucle, de lo contrario no haría la pregunta, porque declararlo dentro del whilebucle no sería una opción, ya que no se compilaría.

Entonces, dado strque no se usa fuera del ciclo, el alcance más pequeño posible strestá dentro del ciclo while.

Entonces, la respuesta es enfáticamente que strabsolutamente debe declararse dentro del ciclo while. Sin peros, sin peros, sin peros.

El único caso en el que podría violarse esta regla es si, por alguna razón, es de vital importancia que cada ciclo de reloj se elimine del código, en cuyo caso es posible que desee considerar crear una instancia de algo en un ámbito externo y reutilizarlo en lugar de volver a crear una instancia en cada iteración de un ámbito interno. Sin embargo, esto no se aplica a su ejemplo, debido a la inmutabilidad de las cadenas en java: siempre se creará una nueva instancia de str al comienzo de su ciclo y tendrá que desecharse al final de la misma, por lo que No hay posibilidad de optimizar allí.

EDITAR: (inyectando mi comentario a continuación en la respuesta)

En cualquier caso, la forma correcta de hacer las cosas es escribir todo su código correctamente, establecer un requisito de rendimiento para su producto, medir su producto final en función de este requisito y, si no lo cumple, ir a optimizar las cosas. Y lo que generalmente sucede es que encuentra formas de proporcionar algunas optimizaciones algorítmicas agradables y formales en solo un par de lugares que hacen que nuestro programa cumpla con sus requisitos de rendimiento en lugar de tener que revisar toda su base de código y modificar y piratear cosas en para apretar los ciclos de reloj aquí y allá.

Mike Nakis
fuente
2
Consulta sobre el último párrafo: si se trata de otra cadena, que no es inmutable, ¿afecta?
Harry Joy el
1
@HarryJoy Sí, por supuesto, tome por ejemplo StringBuilder, que es mutable. Si usa un StringBuilder para construir una nueva cadena en cada iteración del bucle, entonces podría optimizar las cosas asignando el StringBuilder fuera del bucle. Pero aún así, esta no es una práctica aconsejable. Si lo haces sin una muy buena razón, es una optimización prematura.
Mike Nakis
77
@HarryJoy La forma correcta de hacer las cosas es escribir todo su código correctamente , establecer un requisito de rendimiento para su producto, medir su producto final con este requisito y, si no lo satisface, ir a optimizar las cosas. ¿Y sabes qué? Por lo general, podrá proporcionar algunas optimizaciones algorítmicas agradables y formales en solo un par de lugares que harán el truco en lugar de tener que recorrer toda su base de código y ajustar y piratear cosas para apretar los ciclos de reloj aquí y allá.
Mike Nakis
2
@ MikeNakis, creo que estás pensando en un ámbito muy limitado.
Siten
55
Verá, las CPU modernas de varios gigahercios, múltiples núcleos, canalizadas y de múltiples niveles de memoria caché nos permiten centrarnos en seguir las mejores prácticas sin tener que preocuparnos por los ciclos de reloj. Además, la optimización solo es aconsejable si y solo si se ha determinado que es necesario, y cuando es necesario, un par de ajustes altamente localizados generalmente lograrán el rendimiento deseado, por lo que no es necesario ensuciar todo nuestro código con pequeños trucos en nombre del rendimiento.
Mike Nakis
293

Comparé el código de bytes de esos dos ejemplos (similares):

Veamos 1. ejemplo :

package inside;

public class Test {
    public static void main(String[] args) {
        while(true){
            String str = String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

después javac Test.java, javap -c Testobtendrás:

public class inside.Test extends java.lang.Object{
public inside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

Veamos 2. ejemplo :

package outside;

public class Test {
    public static void main(String[] args) {
        String str;
        while(true){
            str =  String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

después javac Test.java, javap -c Testobtendrás:

public class outside.Test extends java.lang.Object{
public outside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

Las observaciones muestran que no hay diferencia entre esos dos ejemplos. Es el resultado de las especificaciones JVM ...

Pero en nombre de la mejor práctica de codificación, se recomienda declarar la variable en el alcance más pequeño posible (en este ejemplo, está dentro del bucle, ya que este es el único lugar donde se usa la variable).

PrimosK
fuente
3
Es el resultado de la JVM Soecification, no la 'optimización del compilador'. Las ranuras de pila requeridas por un método se asignan todas al ingresar al método. Así se especifica el código de bytes.
Marqués de Lorne
2
@Arhimed hay una razón más para ponerlo dentro del bucle (o simplemente '{}' bloque): el compilador reutilizará la memoria asignada en el marco de la pila para la variable en otro ámbito si declara en ese otro ámbito algo sobre variable .
Serge
1
Si se repite a través de una lista de objetos de datos, ¿hará alguna diferencia para la mayor parte de los datos? Probablemente 40 mil.
Mithun Khatri
77
Para cualquiera de ustedes finalamantes: declarar strcomo finalen el insidecaso del paquete tampoco hace diferencia =)
skia.heliou
27

Declarar objetos en el ámbito más pequeño mejora la legibilidad .

El rendimiento no importa para los compiladores de hoy. (En este escenario)
Desde una perspectiva de mantenimiento, la segunda opción es mejor.
Declare e inicialice las variables en el mismo lugar, en el ámbito más estrecho posible.

Como dijo Donald Ervin Knuth :

"Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal"

es decir, la situación en la que un programador deja que las consideraciones de rendimiento afecten el diseño de un fragmento de código. Esto puede dar como resultado un diseño que no es tan limpio como podría haber sido o un código incorrecto, porque la optimización complica el código y el optimizador distrae al programador .

Chandra Sekhar
fuente
1
"La segunda opción tiene un rendimiento ligeramente más rápido" => ¿la has medido? Según una de las respuestas, el código de bytes es el mismo, así que no veo cómo el rendimiento podría ser diferente.
Assylias
Lo siento, pero eso no es realmente el camino correcto para probar el rendimiento de un programa Java (y cómo se puede probar el funcionamiento de un bucle infinito de todos modos?)
assylias
Estoy de acuerdo con sus otros puntos, es solo que creo que no hay diferencia de rendimiento.
Assylias
11

si quieres usar el strbucle externo también; declararlo afuera. de lo contrario, la segunda versión está bien.

Azodioso
fuente
11

Pase a la respuesta actualizada ...

Para aquellos que se preocupan por el rendimiento, saque System.out y limite el ciclo a 1 byte. Usando doble (prueba 1/2) y usando Cadena (3/4), los tiempos transcurridos en milisegundos se muestran a continuación con Windows 7 Professional de 64 bits y JDK-1.7.0_21. Los códigos de bytes (que también se proporcionan a continuación para test1 y test2) no son lo mismo. Era demasiado vago para probar con objetos mutables y relativamente complejos.

doble

Prueba1 tomó: 2710 ms

Test2 tomó: 2790 ms

Cadena (solo reemplace doble con cadena en las pruebas)

Test3 tomó: 1200 ms

Test4 tomó: 3000 ms

Compilar y obtener bytecode

javac.exe LocalTest1.java

javap.exe -c LocalTest1 > LocalTest1.bc


public class LocalTest1 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        double test;
        for (double i = 0; i < 1000000000; i++) {
            test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }

}

public class LocalTest2 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (double i = 0; i < 1000000000; i++) {
            double test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }
}


Compiled from "LocalTest1.java"
public class LocalTest1 {
  public LocalTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore        5
       7: dload         5
       9: ldc2_w        #3                  // double 1.0E9d
      12: dcmpg
      13: ifge          28
      16: dload         5
      18: dstore_3
      19: dload         5
      21: dconst_1
      22: dadd
      23: dstore        5
      25: goto          7
      28: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      31: lstore        5
      33: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: new           #6                  // class java/lang/StringBuilder
      39: dup
      40: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      43: ldc           #8                  // String Test1 Took:
      45: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      48: lload         5
      50: lload_1
      51: lsub
      52: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      55: ldc           #11                 // String  msecs
      57: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      60: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      63: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      66: return
}


Compiled from "LocalTest2.java"
public class LocalTest2 {
  public LocalTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore_3
       6: dload_3
       7: ldc2_w        #3                  // double 1.0E9d
      10: dcmpg
      11: ifge          24
      14: dload_3
      15: dstore        5
      17: dload_3
      18: dconst_1
      19: dadd
      20: dstore_3
      21: goto          6
      24: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      27: lstore_3
      28: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: new           #6                  // class java/lang/StringBuilder
      34: dup
      35: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      38: ldc           #8                  // String Test1 Took:
      40: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      43: lload_3
      44: lload_1
      45: lsub
      46: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      49: ldc           #11                 // String  msecs
      51: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      54: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      57: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      60: return
}

RESPUESTA ACTUALIZADA

Realmente no es fácil comparar el rendimiento con todas las optimizaciones de JVM. Sin embargo, es algo posible. Mejor prueba y resultados detallados en Google Caliper

  1. Algunos detalles en el blog: ¿Debería declarar una variable dentro de un ciclo o antes del ciclo?
  2. Repositorio de GitHub: https://github.com/gunduru/jvdt
  3. Resultados de la prueba para caso doble y bucle de 100M (y sí, todos los detalles de JVM): https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

Declarado antes de 1,759.209 Declarado dentro de 2,242.308

  • Declarado antes de 1.759,209 ns
  • Declarado en el interior 2.242,308 ns

Código de prueba parcial para doble declaración

Esto no es idéntico al código anterior. Si solo codifica un bucle ficticio, JVM lo omite, por lo que al menos debe asignar y devolver algo. Esto también se recomienda en la documentación de Caliper.

@Param int size; // Set automatically by framework, provided in the Main
/**
* Variable is declared inside the loop.
*
* @param reps
* @return
*/
public double timeDeclaredInside(int reps) {
    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Declaration and assignment */
        double test = i;

        /* Dummy assignment to fake JVM */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

/**
* Variable is declared before the loop.
*
* @param reps
* @return
*/
public double timeDeclaredBefore(int reps) {

    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Actual test variable */
    double test = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Assignment */
        test = i;

        /* Not actually needed here, but we need consistent performance results */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

Resumen: declarado antes indica un mejor rendimiento, realmente pequeño, y va en contra del principio de alcance más pequeño. JVM debería hacer esto por ti

Onur Günduru
fuente
Metodología de prueba no válida y no proporciona ninguna explicación de sus resultados.
Marqués de Lorne
1
@EJP Esto debería ser bastante claro para aquellos que estén interesados ​​en el tema. La metodología se toma de la respuesta de PrimosK para proporcionar información más útil. Para ser sincero, no tengo idea de cómo mejorar esta respuesta, ¿tal vez pueda hacer clic en editar y mostrarnos cómo hacerlo correctamente?
Onur Günduru
2
1) Java Bytecode se optimiza (reordenado, colapsado, etc.) en tiempo de ejecución, así que no te preocupes demasiado por lo que está escrito en los archivos .class. 2) hay 1.000.000.000 de carreras para obtener una ganancia de rendimiento de 2.8s, lo que equivale a 2.8ns por carrera versus un estilo de programación seguro y adecuado. Un claro ganador para mí. 3) Dado que no proporciona información sobre el calentamiento, sus tiempos son bastante inútiles.
Codificado el
@Hardcoded mejores pruebas / micro benchmarking con calibrador solo para bucles dobles y de 100M. Resultados en línea, si desea otros casos, no dude en editarlos.
Onur Günduru
Gracias, esto elimina los puntos 1) y 3). Pero incluso si el tiempo subió a ~ 5ns por ciclo, todavía es un momento para ignorar. En teoría, existe un pequeño potencial de optimización, en realidad, lo que haces por ciclo suele ser mucho más costoso. Por lo tanto, el potencial sería de unos segundos como máximo en una carrera de algunos minutos o incluso horas. Hay otras opciones con mayor potencial disponible (por ejemplo, Fork / Join, Streams paralelos) que comprobaría antes de pasar tiempo en este tipo de optimizaciones de bajo nivel.
Codificado el
7

Una solución a este problema podría ser proporcionar un alcance variable que encapsule el ciclo while:

{
  // all tmp loop variables here ....
  // ....
  String str;
  while(condition){
      str = calculateStr();
      .....
  }
}

Serían automáticamente desreferenciados cuando finalice el alcance externo.

Morten Madsen
fuente
6

En el interior, cuanto menos alcance sea visible la variable, mejor.

Jan Zyka
fuente
5

Si no necesita usar el strbucle while (relacionado con el alcance), entonces la segunda condición, es decir

  while(condition){
        String str = calculateStr();
        .....
    }

es mejor ya que si define un objeto en la pila solo si conditiones verdadero. Es decir, úsalo si lo necesitas

Cratilo
fuente
2
Tenga en cuenta que incluso en la primera variante, no se construye ningún objeto si la condición es falsa.
Philipp Wendler
@ Phillip: Sí, tienes razón. Culpa mía. Estaba pensando como está ahora. ¿Qué piensas?
Cratylus
1
Bueno, "definir un objeto en la pila" es un término algo extraño en el mundo de Java. Además, la asignación de una variable en la pila suele ser un noop en tiempo de ejecución, entonces, ¿por qué molestarse? El alcance para ayudar al programador es el verdadero problema.
Philipp Wendler
3

Creo que el mejor recurso para responder a su pregunta sería la siguiente publicación:

¿Diferencia entre declarar variables antes o en bucle?

Según tengo entendido, esto dependería del idioma. IIRC Java optimiza esto, por lo que no hay ninguna diferencia, pero JavaScript (por ejemplo) hará toda la asignación de memoria cada vez en el bucle. En Java, particularmente, creo que el segundo se ejecutará más rápido cuando termine el perfil.

Naveen Goyal
fuente
3

Como mucha gente ha señalado,

String str;
while(condition){
    str = calculateStr();
    .....
}

NO es mejor que esto:

while(condition){
    String str = calculateStr();
    .....
}

Por lo tanto, no declare variables fuera de su alcance si no lo está reutilizando ...

Pavana
fuente
1
excepto probablemente de esta manera: enlace
Dainius Kreivys
2

Declarar String str fuera del bucle wile permite que se haga referencia dentro y fuera del bucle while. Declarar String str dentro del ciclo while permite que solo se haga referencia dentro del ciclo while.

Jay Tomten
fuente
1

Según la guía de desarrollo de Google Android, el alcance variable debe ser limitado. Por favor revise este enlace:

Alcance variable límite

James Jithin
fuente
1

La strvariable estará disponible y reservará algo de espacio en la memoria incluso después de ejecutarse debajo del código.

 String str;
    while(condition){
        str = calculateStr();
        .....
    }

La strvariable no estará disponible y también se liberará la memoria que se asignó para la strvariable en el siguiente código.

while(condition){
    String str = calculateStr();
    .....
}

Si seguimos el segundo seguramente esto reducirá la memoria de nuestro sistema y aumentará el rendimiento.

Ganesa Vijayakumar
fuente
0

Declarar dentro del ciclo limita el alcance de la variable respectiva. Todo depende del requisito del proyecto en el alcance de la variable.

ab02
fuente
0

En verdad, la pregunta mencionada anteriormente es un problema de programación. ¿Cómo le gustaría programar su código? ¿Dónde necesita el 'STR' para acceder? No tiene sentido declarar una variable que se usa localmente como una variable global. Conceptos básicos de programación, creo.

Abhishek Bhandari
fuente
-1

Estos dos ejemplos dan como resultado lo mismo. Sin embargo, el primero le proporciona el uso de la strvariable fuera del ciclo while; El segundo no lo es.

olyanren
fuente
-1

Advertencia para casi todos en esta pregunta: Aquí hay un código de muestra en el que dentro del bucle puede ser fácilmente 200 veces más lento en mi computadora con Java 7 (y el consumo de memoria también es ligeramente diferente). Pero se trata de la asignación y no solo del alcance.

public class Test
{
    private final static int STUFF_SIZE = 512;
    private final static long LOOP = 10000000l;

    private static class Foo
    {
        private long[] bigStuff = new long[STUFF_SIZE];

        public Foo(long value)
        {
            setValue(value);
        }

        public void setValue(long value)
        {
            // Putting value in a random place.
            bigStuff[(int) (value % STUFF_SIZE)] = value;
        }

        public long getValue()
        {
            // Retrieving whatever value.
            return bigStuff[STUFF_SIZE / 2];
        }
    }

    public static long test1()
    {
        long total = 0;

        for (long i = 0; i < LOOP; i++)
        {
            Foo foo = new Foo(i);
            total += foo.getValue();
        }

        return total;
    }

    public static long test2()
    {
        long total = 0;

        Foo foo = new Foo(0);
        for (long i = 0; i < LOOP; i++)
        {
            foo.setValue(i);
            total += foo.getValue();
        }

        return total;
    }

    public static void main(String[] args)
    {
        long start;

        start = System.currentTimeMillis();
        test1();
        System.out.println(System.currentTimeMillis() - start);

        start = System.currentTimeMillis();
        test2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

Conclusión: Dependiendo del tamaño de la variable local, la diferencia puede ser enorme, incluso con variables no tan grandes.

Solo para decir que a veces, fuera o dentro del circuito sí importa.

rt15
fuente
1
Claro, el segundo es más rápido, pero estás haciendo cosas diferentes: test1 está creando muchos Foo-Objects con grandes matrices, test2 no. test2 está reutilizando el mismo objeto Foo una y otra vez, lo que podría ser peligroso en entornos multiproceso.
Codificado el
¿Peligroso en un entorno multiproceso? Por favor explique por qué. Estamos hablando de una variable local. Se crea en cada llamada del método.
rt15
Si pasa el Foo-Object a una operación que está procesando los datos de forma asincrónica, la operación aún podría estar funcionando en la instancia de Foo mientras está cambiando los datos en ella. Ni siquiera tiene que ser multiproceso para tener efectos secundarios. Por lo tanto, la reutilización de instancias es bastante peligrosa cuando no se sabe quién sigue utilizando la instancia
Codificado el
Ps: su método setValue debería ser bigStuff[(int) (value % STUFF_SIZE)] = value;(Pruebe un valor de 2147483649L)
Codificado
Hablando de efectos secundarios: ¿ha comparado los resultados de sus métodos?
Codificado el
-1

Creo que el tamaño del objeto también importa. En uno de mis proyectos, habíamos declarado e inicializado una gran matriz bidimensional que estaba haciendo que la aplicación lanzara una excepción de falta de memoria. En su lugar, retiramos la declaración del ciclo y borramos la matriz al comienzo de cada iteración.

Sanjit
fuente
-2

Corre el riesgo de NullPointerExceptionque su calculateStr()método devuelva nulo y luego intente llamar a un método en str.

De manera más general, evite tener variables con un valor nulo . Es más fuerte para los atributos de clase, por cierto.

Rémi Doolaeghe
fuente
2
Esto no está relacionado con la pregunta. La probabilidad de NullPointerException (en futuras llamadas a funciones) no dependerá de cómo se declare una variable.
Desert Ice
1
No lo creo, porque la pregunta es "¿Cuál es la mejor manera de hacerlo?". En mi humilde opinión, preferiría un código más seguro.
Rémi Doolaeghe
1
No hay ningún riesgo de un NullPointerException.Si este código intentara return str;encontrar un error de compilación.
Marqués de Lorne