Question

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 qu'une déclaration de variable répétée (exemple b ) engendre plus de temps système en théorie , mais que les compilateurs sont suffisamment intelligents pour que cela ne soit pas grave. L'exemple b présente l'avantage d'être plus compact et de limiter la portée de la variable à son utilisation. Malgré tout, j’ai tendance à coder selon l’exemple a .

Modifier: Le cas de Java m'intéresse particulièrement.

Était-ce utile?

La solution

Quel est le meilleur, a ou b ?

Du point de vue des performances, 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 préférable. 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.

Autres conseils

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 cela. Je coderais cela aussi, mais je dirais que cela n’a pas vraiment d’importance.

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 externe , mais chacune a sa propre variable interne distincte.

Voici 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 j’obtiens de .NET Reflector lorsque 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 ont exactement la même apparence 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. Il existe des cas où vous pouvez optimiser le code de gestion, par exemple si vous devez concaténer un grand nombre de chaînes en utilisant uniquement chaîne a; a + = anotherstring [i] vs 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 il peut à peu près optimiser les choses de base pour vous.

Ceci est un piège 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...

Ceci 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 un = 0 , vous obtenez ce que vous pouvez 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...

J'ai fait un test simple:

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

vs

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é le main () de ces deux codes et c'est 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

vs

   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 que le résultat. n'est-ce pas une preuve que les deux codes produisent la même chose?

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

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

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

Cela dépend de la langue - C # IIRC optimise cela, donc il n'y a pas de différence, mais JavaScript (par exemple) fera tout le shebang d'allocation de mémoire à chaque fois.

À mon avis, b est la meilleure structure. En a, la dernière valeur de intermediateResult demeure après la fin de votre boucle.

Modifier: 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,

Je pense 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 en est que vous voulez vous assurer que la variable déclarée est utilisée uniquement dans votre boucle.

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.

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 la portée des variables à l'endroit où elles sont nécessaires, ce qui est une bonne chose.
  • Java est suffisamment optimisé pour ne faire aucune différence significative en termes de performances. IIRC, la seconde forme est peut-être encore plus rapide.

Quoi qu’il en soit, il entre dans la catégorie de l’optimisation prématurée qui repose sur la qualité du compilateur et / ou de la machine virtuelle Java.

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 essentiellement 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 bien plus clairement aux lecteurs que la variable n’est pas et ne peut pas être utilisée après la boucle. De plus, la version b est beaucoup plus facilement refactorisée. 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'y a aucun effet secondaire à une telle refactorisation.

Par conséquent, la version a me gêne au plus haut point, car elle n’a aucun avantage et rend beaucoup plus difficile la raisonnement sur le code ...

Eh bien, vous pouvez toujours créer 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);
    }
}

Ainsi, vous déclarez la variable une seule fois et elle mourra lorsque vous quitterez la boucle.

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, il faut non seulement créer l'objet pour chaque itération, mais également attribuer une nouvelle référence à chaque objet. Il semble que si le ramasse-miettes est lent, vous aurez un tas de références en suspens qui doivent être nettoyées.

Cependant, si vous avez ceci:

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

Ensuite, vous ne créez qu'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 dans ce cas, il n’ya qu’une référence en suspens à traiter.

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

Ma pratique est la suivante:

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

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

Du point de vue des performances, 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 seconde.

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 confronté à 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, en particulier lorsque vous travaillez avec des bibliothèques sources proches.

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 doit être globale?

Il est logique de déclarer la variable une seule fois, globalement, et non 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 d'initialisation 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.

J'ai testé JS avec Node 4.0.0 si quelqu'un était intéressé. La déclaration en dehors de la boucle s'est traduite par 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)

c'est la meilleure forme

double intermediateResult;
int i = byte.MinValue;

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

1) a ainsi déclaré une fois les deux variables, et non chacune pour le cycle. 2) la mission est plus féroce que toutes les autres options. 3) La règle de la meilleure pratique est donc toute déclaration en dehors de l'itération pour.

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

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

J'avais cette même question depuis longtemps. J'ai donc testé un morceau de code encore plus simple.

Conclusion: pour ces cas , la différence de performances est NON .

Etui de la boucle extérieure

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

Etui de la 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, le même Test.class

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

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

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

Etui de la 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 faites très attention, seuls les emplacements affectés à i et intermediateResult dans LocalVariableTable sont permutés en tant que un 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 temps d'accès différent.

BONUS

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

Cas de travail zéro

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

Même si je sais que mon compilateur est suffisamment intelligent, je n’aimerais pas compter sur lui et utiliserai la variante a).

La variante b) n’a de sens que si vous devez absolument rendre le intermediateResult indisponible après le corps de la boucle. Mais je ne peux pas imaginer une telle situation désespérée, de toute façon ....

EDIT: Jon Skeet a fait un très bon point, montrant que la déclaration de variable dans une boucle peut faire une différence sémantique réelle.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top