Déclaration de variables à l'intérieur ou à l'extérieur d'une boucle


236

Pourquoi les éléments suivants fonctionnent-ils bien?

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

Mais celui-ci est dit dangereux / incorrect:

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

Est-il nécessaire de déclarer des variables en dehors de la boucle?

Réponses:


289

La portée des variables locales doit toujours être la plus petite possible.

Dans votre exemple, je présume qu'il strn'est pas utilisé en dehors de la whileboucle, sinon vous ne poseriez pas la question, car le déclarer à l'intérieur de la whileboucle ne serait pas une option, car il ne compilerait pas.

Donc, comme il strn'est pas utilisé en dehors de la boucle, la plus petite étendue possible pour se strtrouve dans la boucle while.

Donc, la réponse est catégoriquement qui strdoit absolument être déclarée dans la boucle while. Pas de si, pas de and, pas de mais.

Le seul cas où cette règle pourrait être violée est si, pour une raison quelconque, il est d'une importance vitale que chaque cycle d'horloge soit retiré du code, auquel cas vous voudrez peut-être envisager d'instancier quelque chose dans une portée externe et de le réutiliser au lieu de la ré-instanciation à chaque itération d'une portée intérieure. Cependant, cela ne s'applique pas à votre exemple, en raison de l'immuabilité des chaînes en java: une nouvelle instance de str sera toujours créée au début de votre boucle et devra être jetée à la fin de celle-ci, donc là il n'y a aucune possibilité d'optimiser là-bas.

EDIT: (en injectant mon commentaire ci-dessous dans la réponse)

Dans tous les cas, la bonne façon de faire les choses est d'écrire correctement tout votre code, d'établir une exigence de performance pour votre produit, de mesurer votre produit final par rapport à cette exigence, et s'il ne la satisfait pas, alors allez optimiser les choses. Et ce qui finit généralement par se produire, c'est que vous trouvez des moyens de fournir des optimisations algorithmiques agréables et formelles à quelques endroits qui permettent à notre programme de répondre à ses exigences de performances au lieu d'avoir à parcourir toute votre base de code et de modifier et de pirater des choses dans afin de serrer les cycles d'horloge ici et là.


2
Requête sur le dernier paragraphe: S'il s'agissait d'un autre alors String qui n'est pas immuable, cela affecte-t-il?
Harry Joy

1
@HarryJoy Oui, bien sûr, prenez par exemple StringBuilder, qui est mutable. Si vous utilisez un StringBuilder pour créer une nouvelle chaîne à chaque itération de la boucle, vous pouvez optimiser les choses en allouant le StringBuilder en dehors de la boucle. Mais encore, ce n'est pas une pratique recommandée. Si vous le faites sans très bonne raison, c'est une optimisation prématurée.
Mike Nakis

7
@HarryJoy La bonne façon de faire les choses est d'écrire correctement tout votre code , d'établir une exigence de performance pour votre produit, de mesurer votre produit final par rapport à cette exigence, et s'il ne la satisfait pas, d'aller optimiser les choses. Et tu sais quoi? Vous serez généralement en mesure de fournir des optimisations algorithmiques agréables et formelles à quelques endroits, ce qui fera l'affaire au lieu d'avoir à parcourir toute votre base de code et à modifier et pirater des choses afin de serrer les cycles d'horloge ici et là.
Mike Nakis

2
@MikeNakis Je pense que vous pensez dans un cadre très étroit.
Siten

5
Vous voyez, les processeurs modernes multi-gigahertz, multi-cœurs, pipelinés et cache-mémoire à plusieurs niveaux nous permettent de nous concentrer sur les meilleures pratiques sans avoir à se soucier des cycles d'horloge. De plus, l'optimisation n'est recommandée que si et seulement s'il a été déterminé qu'elle est nécessaire, et lorsque cela est nécessaire, quelques ajustements hautement localisés atteindront généralement les performances souhaitées, il n'est donc pas nécessaire de jeter tout notre code. avec de petits hacks au nom de la performance.
Mike Nakis

293

J'ai comparé le code d'octet de ces deux exemples (similaires):

Regardons 1. exemple :

package inside;

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

après javac Test.java, javap -c Testvous obtiendrez:

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

}

Regardons 2. exemple :

package outside;

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

après javac Test.java, javap -c Testvous obtiendrez:

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

}

Les observations montrent qu'il n'y a pas de différence entre ces deux exemples. C'est le résultat des spécifications JVM ...

Mais au nom de la meilleure pratique de codage, il est recommandé de déclarer la variable dans la plus petite portée possible (dans cet exemple, elle est à l'intérieur de la boucle, car c'est le seul endroit où la variable est utilisée).


3
C'est le résultat de la JVM Soecification, pas de «l'optimisation du compilateur». Les emplacements de pile requis par une méthode sont tous alloués à l'entrée de la méthode. C'est ainsi que le bytecode est spécifié.
Marquis de Lorne

2
@Arhimed il y a une raison de plus pour le mettre dans la boucle (ou juste le bloc '{}'): le compilateur réutilisera la mémoire allouée dans le cadre de la pile pour la variable dans une autre portée si vous déclarez dans cette autre portée une sur-variable .
Serge

1
Si son bouclage à travers une liste d'objets de données, cela fera-t-il une différence pour la masse des données? Probablement 40 milliers.
Mithun Khatri

7
Pour n'importe lequel d'entre vous finalamoureux: déclarer strcomme finaldans le insidecas du paquet ne fait également aucune différence =)
skia.heliou

27

La déclaration d'objets dans la plus petite étendue améliore la lisibilité .

Les performances n'ont pas d'importance pour les compilateurs d'aujourd'hui (dans ce scénario)
Du point de vue de la maintenance, la 2e option est meilleure.
Déclarez et initialisez les variables au même endroit, dans la portée la plus étroite possible.

Comme Donald Ervin Knuth l'a dit:

"Il faut oublier les petites efficacités, disons environ 97% du temps: l'optimisation prématurée est la racine de tout mal"

c.-à-d.) situation dans laquelle un programmeur laisse des considérations de performances affecter la conception d'un morceau de code. Cela peut entraîner une conception qui n'est pas aussi propre qu'elle aurait pu l'être ou un code incorrect, car le code est compliqué par l' optimisation et le programmeur est distrait par l' optimisation .


1
"La 2ème option a des performances légèrement plus rapides" => l'avez-vous mesurée? Selon l'une des réponses, le bytecode est le même, donc je ne vois pas comment les performances pourraient être différentes.
assylias

Je suis désolé mais ce n'est vraiment pas la bonne façon de tester les performances d'un programme java (et comment pouvez-vous tester les performances d'une boucle infinie de toute façon?)
assylias

Je suis d'accord avec vos autres points - c'est juste que je crois qu'il n'y a pas de différence de performance.
assylias

11

si vous souhaitez également utiliser strlooop extérieur; le déclarer à l'extérieur. sinon, la 2ème version est très bien.


11

Veuillez passer à la réponse mise à jour ...

Pour ceux qui se soucient des performances, sortez le System.out et limitez la boucle à 1 octet. En utilisant double (test 1/2) et String (3/4), le temps écoulé en millisecondes est indiqué ci-dessous avec Windows 7 Professionnel 64 bits et JDK-1.7.0_21. Les bytecodes (également donnés ci-dessous pour test1 et test2) ne sont pas identiques. J'étais trop paresseux pour tester avec des objets mutables et relativement complexes.

double

Test1 a pris: 2710 ms

Test2 a pris: 2790 ms

String (remplacez simplement double par string dans les tests)

Test3 a pris: 1200 ms

Test4 a pris: 3000 ms

Compilation et obtention du 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
}

RÉPONSE MISE À JOUR

Il n'est vraiment pas facile de comparer les performances avec toutes les optimisations JVM. Cependant, c'est quelque peu possible. Meilleur test et résultats détaillés dans Google Caliper

  1. Quelques détails sur le blog: Devriez-vous déclarer une variable à l'intérieur d'une boucle ou avant la boucle?
  2. Référentiel GitHub: https://github.com/gunduru/jvdt
  3. Résultats des tests pour le double boîtier et la boucle 100M (et oui tous les détails JVM): https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

Déclaré Avant 1759,209 Déclaré Intérieur 2242,308

  • Déclaré Avant 1759,209 ns
  • Déclaré à l'intérieur 2242,308 ns

Code d'essai partiel pour double déclaration

Ce n'est pas identique au code ci-dessus. Si vous codez simplement une boucle factice, la JVM la saute, vous devez donc au moins affecter et renvoyer quelque chose. Ceci est également recommandé dans la documentation 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;
}

Résumé: declareBefore indique une meilleure performance - vraiment minuscule - et c'est contre le principe de la plus petite portée. JVM devrait en fait le faire pour vous


Méthodologie de test non valide et vous ne fournissez aucune explication de vos résultats.
Marquis de Lorne

1
@EJP Cela devrait être assez clair pour ceux qui s'intéressent au sujet. La méthodologie est tirée de la réponse de PrimosK pour fournir des informations plus utiles. Pour être honnête, je ne sais pas comment améliorer cette réponse, peut-être pouvez-vous cliquer sur modifier et nous montrer comment le faire correctement?
Onur Günduru

2
1) Java Bytecode est optimisé (réorganisé, réduit, etc.) à l'exécution, alors ne vous souciez pas trop de ce qui est écrit dans les fichiers .class. 2) il y a 1.000.000.000 de courses pour gagner une performance de 2,8 secondes, c'est donc environ 2,8 ns par course contre un style de programmation sûr et approprié. Un gagnant clair pour moi. 3) Puisque vous ne fournissez aucune information sur l'échauffement, vos horaires sont tout à fait inutiles.
Codé en dur le

@Hardcoded meilleurs tests / micro benchmarking avec étrier uniquement pour les boucles doubles et 100M. Résultats en ligne, si vous souhaitez modifier d'autres cas, n'hésitez pas à les modifier.
Onur Günduru

Merci, cela élimine les points 1) et 3). Mais même si le temps passait à ~ 5 ns par cycle, c'est encore un temps à ignorer. Il y a un petit potentiel d'optimisation en théorie, en réalité, ce que vous faites par cycle est généralement beaucoup plus cher. Le potentiel serait donc de quelques secondes au maximum en quelques minutes, voire quelques heures. Il y a d'autres options avec un potentiel plus élevé disponibles (par exemple Fork / Join, Streams parallèles) que je voudrais vérifier avant de passer du temps sur ce type d'optimisations de bas niveau.
Codé en dur le

7

Une solution à ce problème pourrait être de fournir une portée variable encapsulant la boucle while:

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

Ils seraient automatiquement dé-référence lorsque la portée externe se termine.


6

A l'intérieur, moins la portée est visible, mieux c'est.


5

Si vous n'avez pas besoin d'utiliser la strboucle after the while (liée à la portée), la deuxième condition

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

est préférable car si vous définissez un objet sur la pile uniquement si le conditionest vrai. C'est à dire l'utiliser si vous en avez besoin


2
Notez que même dans la première variante, aucun objet n'est construit si la condition est fausse.
Philipp Wendler

@ Phillip: Oui, vous avez raison. Ma faute. Je pensais comme c'est maintenant. Que pensez-vous?
Cratylus

1
Eh bien, «définir un objet sur la pile» est un terme quelque peu étrange dans le monde Java. En outre, l'allocation d'une variable sur la pile est généralement un noop au moment de l'exécution, alors pourquoi s'embêter? La portée pour aider le programmeur est le vrai problème.
Philipp Wendler


3

Comme de nombreuses personnes l'ont souligné,

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

n'est PAS mieux que ça:

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

Ne déclarez donc pas de variables en dehors de leur portée si vous ne les réutilisez pas ...


1
sauf probablement de cette façon: lien
Dainius Kreivys

2

Déclarer String str en dehors de la boucle wile permet de le référencer à l'intérieur et à l'extérieur de la boucle while. La déclaration de String str à l'intérieur de la boucle while permet de la référencer uniquement à l'intérieur de la boucle while.




1

La strvariable sera disponible et réservera de l'espace en mémoire même après avoir été exécutée sous le code.

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

La strvariable ne sera pas disponible et également la mémoire sera libérée qui a été allouée pour la strvariable dans le code ci-dessous.

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

Si nous avons suivi le deuxième, cela réduira sûrement la mémoire de notre système et augmentera les performances.


0

La déclaration à l'intérieur de la boucle limite la portée de la variable respective. Tout dépend de l'exigence du projet sur l'étendue de la variable.


0

Vraiment, la question énoncée ci-dessus est un problème de programmation. Comment souhaitez-vous programmer votre code? Où avez-vous besoin d'accéder au «STR»? Il est inutile de déclarer une variable qui est utilisée localement comme variable globale. Les bases de la programmation je crois.


-1

Ces deux exemples aboutissent à la même chose. Cependant, le premier vous permet d'utiliser la strvariable en dehors de la boucle while; le second ne l'est pas.


-1

Avertissement pour presque tout le monde dans cette question: voici un exemple de code où à l'intérieur de la boucle, il peut facilement être 200 fois plus lent sur mon ordinateur avec Java 7 (et la consommation de mémoire est également légèrement différente). Mais c'est une question d'allocation et pas seulement de portée.

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);
    }
}

Conclusion: Selon la taille de la variable locale, la différence peut être énorme, même avec des variables moins importantes.

Juste pour dire que parfois, à l'extérieur ou à l'intérieur de la boucle, c'est important.


1
Bien sûr, le second est plus rapide, mais vous faites des choses différentes: test1 crée beaucoup de Foo-Objects avec de grands tableaux, pas test2. test2 réutilise le même objet Foo encore et encore, ce qui pourrait être dangereux dans des environnements multithreads.
Codé en dur le

Dangereux dans un environnement multithread ??? Veuillez expliquer pourquoi. Nous parlons d'une variable locale. Il est créé à chaque appel de la méthode.
rt15

Si vous transmettez l'objet Foo à une opération qui traite les données de manière asynchrone, l'opération peut toujours fonctionner sur l'instance Foo pendant que vous modifiez les données qu'elle contient. Il n'a même pas besoin d'être multithread pour avoir des effets secondaires. La réutilisation d'instance est donc assez dangereuse, lorsque vous ne savez pas qui utilise toujours l'instance
Hardcoded

Ps: Votre méthode setValue devrait être bigStuff[(int) (value % STUFF_SIZE)] = value;(Essayez une valeur de 2147483649L)
Hardcoded

Parlant des effets secondaires: avez-vous comparé les résultats de vos méthodes?
Codé en dur le

-1

Je pense que la taille de l'objet est également importante. Dans l'un de mes projets, nous avions déclaré et initialisé un grand tableau bidimensionnel qui faisait que l'application rejetait une exception de mémoire insuffisante. Nous avons plutôt déplacé la déclaration hors de la boucle et effacé le tableau au début de chaque itération.


-2

Vous risquez de voir NullPointerExceptionsi votre calculateStr()méthode retourne null , puis vous essayez d'appeler une méthode sur str.

Plus généralement, évitez d'avoir des variables avec une valeur nulle . Il est plus fort pour les attributs de classe, soit dit en passant.


2
Ce n'est aucunement lié à la question. La probabilité de NullPointerException (lors d'appels de fonction futurs) ne dépendrait pas de la façon dont une variable est déclarée.
Desert Ice

1
Je ne pense pas, car la question est "Quelle est la meilleure façon de le faire?". À mon humble avis, je préférerais un code plus sûr.
Rémi Doolaeghe

1
Il n'y a aucun risque de NullPointerException.si ce code tentait de return str;rencontrer une erreur de compilation.
Marquis de Lorne
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.