web-dev-qa-db-fra.com

Différence entre déclarer des variables avant ou en boucle?

Je me suis toujours demandé si, en général, déclarer une variable à jeter avant une boucle, par opposition à plusieurs reprises dans la boucle, faisait une différence (de performance)? Un exemple (assez inutile) en Java:

a) déclaration avant la boucle:

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

b) déclaration (répétée) dans la boucle:

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

Lequel est le meilleur, a ou b

Je soupçonne que la déclaration de variable répétée (exemple b ) crée plus de surcharge en théorie, mais que les compilateurs sont suffisamment intelligents pour que cela n’importe pas. Exemple b a l’avantage d’être plus compact et de limiter la portée de la variable à l’endroit où elle est utilisée. Pourtant, j'ai tendance à coder selon l'exemple a .

Edit:Je suis particulièrement intéressé par le cas de Java.

299
Rabarberski

Quel est le meilleur, a ou b ?

Du point de vue de la performance, vous devez le mesurer. (Et à mon avis, si vous pouvez mesurer une différence, le compilateur n'est pas très bon).

Du point de vue de la maintenance, b est meilleur. Déclarez et initialisez les variables au même endroit, dans la portée la plus étroite possible. Ne laissez pas un vide entre la déclaration et l'initialisation et ne polluez pas les espaces de noms inutiles.

244
Daniel Earwicker

Eh bien, j’ai exécuté vos exemples A et B 20 fois, en bouclant 100 millions de fois. (JVM - 1.5.0)

A: temps d'exécution moyen: 0,074 seconde

B: temps d'exécution moyen: 0,067 seconde

À ma grande surprise, B était légèrement plus rapide… .. Aussi vite que les ordinateurs sont maintenant difficiles à dire si vous pouvez mesurer avec précision ceci… .. Je coderais cela aussi, mais je dirais que cela n'a pas d'importance .

207
Mark

Cela dépend de la langue et de l'utilisation exacte. Par exemple, en C 1, cela ne faisait aucune différence. En C # 2, si la variable locale est capturée par une méthode anonyme (ou une expression lambda en C # 3), cela peut faire une différence très significative.

Exemple:

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

Sortie:

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 différence est que toutes les actions capturent la même variable outer, mais chacune a sa propre variable inner distincte.

66
Jon Skeet

Ce qui suit est ce que j’ai écrit et compilé dans .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);
}

C’est ce que je tire de .NET Reflector when CIL est restitué dans le code.

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

Donc, les deux ressemblent exactement après la compilation. Dans les langues gérées, le code est converti en code CL/octet et, au moment de son exécution, en langage machine. Donc, en langage machine, un double ne peut même pas être créé sur la pile. Il peut s’agir d’un registre car le code indique qu’il s’agit d’une variable temporaire pour la fonction WriteLine. Il y a tout un ensemble de règles d'optimisation pour les boucles. Donc, le gars moyen ne devrait pas s'en inquiéter, surtout dans les langages gérés. Dans certains cas, vous pouvez optimiser le code de gestion, par exemple si vous devez concaténer un grand nombre de chaînes en utilisant uniquement string a; a+=anotherstring[i] et en utilisant StringBuilder. Il y a une très grande différence de performance entre les deux. Il existe de nombreux cas où le compilateur ne peut pas optimiser votre code, car il ne peut pas comprendre ce qui est prévu dans une portée plus grande. Mais cela peut très bien optimiser les choses de base pour vous.

35
particle

Ceci est un gotcha dans VB.NET. Le résultat Visual Basic ne réinitialise pas la variable dans cet exemple:

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

' Output: 0 1 2 3 4...

Cela imprimera 0 pour la première fois (les variables Visual Basic ont des valeurs par défaut lorsqu'elles sont déclarées!) Mais i chaque fois par la suite.

Si vous ajoutez cependant un = 0, vous obtenez ce à quoi vous pouvez vous attendre:

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

'Output: 0 0 0 0 0...
24
Michael Haren

J'ai fait un test simple:

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

contre 

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

J'ai compilé ces codes avec gcc - 5.2.0. Et puis j'ai démonté la main () De ces deux codes et voilà le résultat:

1º:

   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

contre

   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 

Qui sont exactement les mêmes résultats. n'est-ce pas une preuve que les deux codes produisent la même chose?

15
UserX

J'utiliserais toujours A (plutôt que de compter sur le compilateur) et pourrais aussi réécrire pour:

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

Ceci restreint toujours intermediateResult à la portée de la boucle, mais ne redéclare pas à chaque itération.

11
Triptych

Il dépend du langage - IIRC C # optimise cela, donc il n'y a pas de différence, mais JavaScript (par exemple) fera à chaque fois l'allocation de mémoire totale Shebang.

11
annakata

À mon avis, b est la meilleure structure. En a, la dernière valeur de intermediateResult persiste une fois votre boucle terminée.

Edit: Cela ne fait pas beaucoup de différence avec les types de valeur, mais les types de référence peuvent être un peu lourds. Personnellement, j'aime bien que les variables soient déréférencées dès que possible pour le nettoyage, et b le fait pour vous,

6
Powerlord

Eh bien, vous pouvez toujours faire une marge pour cela:

{ //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 cette façon, vous déclarez la variable une seule fois et elle mourra lorsque vous quitterez la boucle.

5
Marcelo Faísca

Il y a une différence en C # si vous utilisez la variable dans un lambda, etc. Mais en général, le compilateur fera la même chose, en supposant que la variable est uniquement utilisée dans la boucle. 

Étant donné qu’elles sont fondamentalement les mêmes: Notez que la version b montre beaucoup plus clairement aux lecteurs que la variable n’est pas et ne peut pas être utilisée après la boucle. De plus, version b est beaucoup plus facilement refactorisé. Il est plus difficile d'extraire le corps de la boucle dans sa propre méthode dans la version a. De plus, la version b vous assure qu’il n’ya aucun effet secondaire à un tel refactoring.

Par conséquent, la version a m'énerve énormément, car elle ne présente aucun avantage et rend le raisonnement du code beaucoup plus difficile ...

5
Mark Sowul

Un collègue préfère le premier formulaire, en disant qu'il s'agit d'une optimisation, préférant réutiliser une déclaration.

Je préfère le second (et essayer de persuader mon collègue! ;-)), après avoir lu cela:

  • Cela réduit le nombre de variables aux endroits où elles sont nécessaires, ce qui est une bonne chose.
  • Java optimise suffisamment pour ne faire aucune différence significative dans les performances. IIRC, peut-être que la deuxième forme est encore plus rapide.

Quoi qu'il en soit, il entre dans la catégorie des optimisations prématurées qui reposent sur la qualité du compilateur et/ou de la machine virtuelle Java.

5
PhiLho

Je suppose que quelques compilateurs pourraient optimiser le même code, mais certainement pas tous. Donc, je dirais que vous êtes mieux avec l'ancien. La dernière raison à cela est si vous voulez vous assurer que la variable déclarée est utilisée seulement dans votre boucle.

5
Stew S

En règle générale, je déclare mes variables dans la portée la plus interne possible. Donc, si vous n'utilisez pas intermediaryResult en dehors de la boucle, je choisirais B.

5
Chris

J'ai toujours pensé que si vous déclariez vos variables à l'intérieur de votre boucle, vous perdiez de la mémoire. Si vous avez quelque chose comme ça:

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

Alors, non seulement l’objet doit être créé pour chaque itération, mais une nouvelle référence doit être allouée pour chaque objet. Il semble que si le ramasse-miettes est lent, vous aurez un tas de références pendantes à nettoyer.

Cependant, si vous avez ceci:

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

Ensuite, vous créez uniquement une seule référence et vous lui affectez un nouvel objet à chaque fois. Bien sûr, il faudra peut-être un peu plus de temps pour que cela sorte de sa portée, mais il n’ya alors qu’une seule référence à traiter.

4
R. Carr

Ma pratique est la suivante: 

  • si le type de variable est simple (int, double, ...) Je préfère la variante b (inside).
    Raison: réduisant la portée de la variable. 

  • si le type de variable n’est pas simple (une sorte de class ou struct) Je préfère variante a (extérieur).
    Raison: réduction du nombre d’appels ctor-dtor.

3
fat

Je pense que cela dépend du compilateur et qu'il est difficile de donner une réponse générale.

3
SquidScareMe

Du point de vue de la performance, l'extérieur est (beaucoup) meilleur.

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

J'ai exécuté les deux fonctions 1 milliard de fois chacune. outside () a pris 65 millisecondes. inside () a pris 1.5 secondes.

1
Alex

C'est une question intéressante. D'après mon expérience, il y a une question ultime à prendre en compte lorsque vous débattez de cette question pour un code:

Y a-t-il une raison pour laquelle la variable devrait être globale?

Il est judicieux de déclarer la variable une seule fois, globalement, par opposition à plusieurs fois localement, car elle est meilleure pour organiser le code et nécessite moins de lignes de code. Cependant, si elle ne doit être déclarée que localement au sein d'une méthode, je l'initialisera dans cette méthode afin qu'il soit clair que la variable est exclusivement pertinente pour cette méthode. Veillez à ne pas appeler cette variable en dehors de la méthode dans laquelle elle est initialisée si vous choisissez la dernière option. Votre code ne saura pas de quoi vous parlez et signalera une erreur.

De plus, il convient également de ne pas dupliquer les noms de variables locales entre différentes méthodes, même si leurs objectifs sont presque identiques; ça devient juste déroutant. 

0
Joshua Siktar

J'ai essayé la même chose dans Go et comparé la sortie du compilateur avec go tool compile -S avec go 1.9.4

Différence zéro, selon la sortie de l'assembleur.

0
SteveOC 64

J'ai eu cette même question pendant une longue période. J'ai donc testé un morceau de code encore plus simple. 

Conclusion: Pour de tels cas, il y a NO différence de performance.

Cas de la boucle extérieure

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

Boucle interne

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

J'ai vérifié le fichier compilé sur le décompilateur IntelliJ et, dans les deux cas, j'ai obtenu le mêmeTest.class

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

J'ai également désassemblé le code pour les deux cas en utilisant la méthode donnée dans cette answer . Je ne montrerai que les parties pertinentes pour la réponse

Cas de la boucle extérieure

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;

Boucle interne

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 vous portez une attention particulière, seuls les Slot affectés à i et intermediateResult dans LocalVariableTable sont échangés en tant que produit de leur ordre d'apparition. La même différence de slot est reflétée dans les autres lignes de code.

  • Aucune opération supplémentaire n'est en cours d'exécution
  • intermediateResult est toujours une variable locale dans les deux cas, il n'y a donc pas de différence de temps d'accès.

PRIME 

Les compilateurs font une tonne d'optimisation, regardez ce qui se passe dans ce cas.

Zéro cas de travail

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

Zéro travail décompilé

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

c'est la meilleure forme

double intermediateResult;
int i = byte.MinValue;

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

1) de cette façon déclarée une fois les deux variables, et non chacune pour le cycle . 2) l’affectation c’est fatser toutes les autres options . 3) La règle de la meilleure pratique est donc toute déclaration en dehors de l’itération pour.

0
luka

A) est une valeur sûre que B) ......... Imaginez si vous initialisez une structure en boucle plutôt que 'int' ou 'float', alors quoi?

comme 

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?
}

Vous êtes certainement obligé de faire face à des problèmes de fuites de mémoire!. Par conséquent, je pense que «A» est un pari plus sûr que «B» est vulnérable à l’accumulation de mémoire, particulièrement en travaillant avec des bibliothèques sources proches. 

0
enthusiasticgeek

J'ai testé pour JS avec Node 4.0.0 si quelqu'un est intéressé. La déclaration en dehors de la boucle a entraîné une amélioration des performances d'environ 0,5 ms en moyenne sur 1 000 essais avec 100 millions d'itérations de boucle par essai. Donc, je vais dire, allez-y et écrivez-le de la manière la plus lisible/maintenable qui soit B, imo. Je mettrais mon code dans un violon, mais j'ai utilisé le module performance-now Node. Voici le code:

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)
0
user137717