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.
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.
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 .
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.
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.
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...
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
2º
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?
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.
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.
À 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,
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.
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 ...
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:
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.
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.
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.
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.
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.
Je pense que cela dépend du compilateur et qu'il est difficile de donner une réponse générale.
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.
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.
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.
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.
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);
}
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.
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.
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)