Differenza tra dichiarare le variabili prima o in loop?

Mi sono sempre chiesto se, in generale, dichiarare una variabile throw-away prima di un loop, al contrario di ripetutamente all’interno del loop, faccia qualche differenza (performance)? Un esempio (abbastanza inutile) in Java:

a) dichiarazione prima del ciclo:

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

b) dichiarazione (ripetutamente) all’interno del loop:

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

Quale è meglio, a o b ?

Sospetto che la dichiarazione di variabili ripetute (esempio b ) crei un sovraccarico in teoria , ma che i compilatori siano abbastanza intelligenti da non avere importanza. L’esempio b ha il vantaggio di essere più compatto e limitare l’ambito della variabile a dove è usato. Tuttavia, tendo a codificare secondo l’esempio a .

Edit: Sono particolarmente interessato al caso Java.

Quale è meglio, a o b ?

Dal punto di vista delle prestazioni, dovresti misurarlo. (E secondo me, se puoi misurare una differenza, il compilatore non è molto buono).

Dal punto di vista della manutenzione, b è migliore. Dichiarare e inizializzare le variabili nello stesso posto, nel più stretto ambito ansible. Non lasciare un buco tra la dichiarazione e l’inizializzazione e non inquinare spazi dei nomi che non è necessario.

Bene, ho eseguito gli esempi A e B 20 volte ciascuno, ripetendo 100 milioni di volte. (JVM – 1.5.0)

A: tempo medio di esecuzione: .074 sec

B: tempo medio di esecuzione: .067 sec

Con mia sorpresa, B era leggermente più veloce. La velocità con cui i computer sono ora è difficile dire se si può misurare con precisione questo. Lo classificherei anche nel modo A, ma direi che non ha molta importanza.

Dipende dalla lingua e dall’uso esatto. Ad esempio, in C # 1 non ha fatto differenza. In C # 2, se la variabile locale viene catturata da un metodo anonimo (o espressione lambda in C # 3) può fare una differenza molto significativa.

Esempio:

 using System; using System.Collections.Generic; class Test { static void Main() { List actions = new List(); 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(); } } } 

Produzione:

 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 differenza è che tutte le azioni catturano la stessa variabile outer , ma ognuna ha una propria variabile inner separata.

Quello che segue è ciò che ho scritto e compilato in .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); } 

Questo è ciò che ottengo da .NET Reflector quando CIL viene restituito al codice.

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

Quindi entrambi sembrano esattamente gli stessi dopo la compilazione. Nel codice delle lingue gestite viene convertito in codice CL / byte e al momento dell'esecuzione viene convertito in linguaggio macchina. Quindi, nel linguaggio macchina, un doppio non può nemmeno essere creato in pila. Potrebbe essere solo un registro poiché il codice riflette che si tratta di una variabile temporanea per la funzione WriteLine . Ci sono un intero set di regole di ottimizzazione solo per i loop. Quindi il ragazzo medio non dovrebbe preoccuparsene, specialmente nelle lingue gestite. Vi sono casi in cui è ansible ottimizzare il codice di gestione, ad esempio, se è necessario concatenare un numero elevato di stringhe utilizzando solo la string a; a+=anotherstring[i] string a; a+=anotherstring[i] vs usando StringBuilder . C'è una grande differenza nelle prestazioni tra entrambi. Ci sono molti casi in cui il compilatore non può ottimizzare il codice, perché non riesce a capire cosa si intende in un ambito più ampio. Ma può praticamente ottimizzare le cose di base per te.

Questo è un trucco in VB.NET. Il risultato di Visual Basic non reinizializzerà la variabile in questo esempio:

 For i as Integer = 1 to 100 Dim j as Integer Console.WriteLine(j) j = i Next ' Output: 0 1 2 3 4... 

Questo verrà stampato 0 la prima volta (le variabili di Visual Basic hanno valori predefiniti quando dichiarati!) Ma i ogni volta dopo.

Se aggiungi a = 0 , però, ottieni ciò che potresti aspettarti:

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

Ho fatto un semplice test:

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

vs

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

Ho compilato questi codici con gcc - 5.2.0. E poi ho smontato il main () di questi due codici e questo è il risultato:

1º:

  0x00000000004004b6 <+0>: push rbp 0x00000000004004b7 <+1>: mov rbp,rsp 0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0 0x00000000004004c1 <+11>: jmp 0x4004cd  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  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  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  0x00000000004004d3 <+29>: mov eax,0x0 0x00000000004004d8 <+34>: pop rbp 0x00000000004004d9 <+35>: ret 

Che sono esattamente lo stesso risultato di asm. non è una prova che i due codici producono la stessa cosa?

Userei sempre A (piuttosto che fare affidamento sul compilatore) e potrei anche riscrivere a:

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

Ciò limita ancora intermediateResult all'ambito del ciclo, ma non lo ripete durante ogni iterazione.

È dipendente dalla lingua – IIRC C # lo ottimizza, quindi non c’è alcuna differenza, ma JavaScript (ad esempio) eseguirà l’intera allocazione della memoria ogni volta.

Secondo me, b è la struttura migliore. In a, l’ultimo valore di intermediateResult rimane invariato dopo che il ciclo è terminato.

Modifica: questo non fa molta differenza con i tipi di valore, ma i tipi di riferimento possono essere piuttosto pesanti. Personalmente, mi piacciono le variabili da deversi non appena ansible per la pulizia, e b lo fa per te,

Sospetto che alcuni compilatori potrebbero ottimizzare entrambi per essere lo stesso codice, ma certamente non tutti. Quindi direi che stai meglio con il primo. L’unico motivo per quest’ultimo è se si vuole assicurare che la variabile dichiarata venga utilizzata solo all’interno del proprio loop.

Come regola generale, dichiaro le mie variabili nel più ampio ambito ansible. Quindi, se non stai usando intermediateResult al di fuori del ciclo, allora andrei con B.

Un collaboratore preferisce il primo modulo, dicendo che è un’ottimizzazione, preferendo riutilizzare una dichiarazione.

Preferisco il secondo (e cerco di persuadere il mio collaboratore! ;-)), avendo letto che:

  • Riduce l’ambito delle variabili dove sono necessarie, il che è una buona cosa.
  • Java ottimizza abbastanza per non fare alcuna differenza significativa nelle prestazioni. IIRC, forse la seconda forma è ancora più veloce.

Ad ogni modo, rientra nella categoria di ottimizzazione prematura che si basa sulla qualità del compilatore e / o della JVM.

C’è una differenza in C # se si sta utilizzando la variabile in un lambda, ecc. Ma in generale il compilatore farà fondamentalmente la stessa cosa, assumendo che la variabile venga utilizzata solo all’interno del ciclo.

Dato che sono fondamentalmente uguali: si noti che la versione b rende molto più evidente ai lettori che la variabile non è, e non può, essere utilizzata dopo il ciclo. Inoltre, la versione b è molto più facilmente rifattorizzata. È più difficile estrarre il corpo del ciclo nel suo metodo nella versione a. Inoltre, la versione b assicura che non vi è alcun effetto collaterale a tale refactoring.

Quindi, la versione a mi infastidisce senza fine, perché non c’è alcun vantaggio e rende molto più difficile ragionare sul codice …

Bene, puoi sempre fare uno scopo per questo:

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

In questo modo dichiarerai la variabile una sola volta e morirà quando lascerai il ciclo.

Ho sempre pensato che se dichiari le tue variabili all’interno del tuo ciclo, stai sprecando memoria. Se hai qualcosa del genere:

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

Quindi non solo l’object deve essere creato per ogni iterazione, ma deve esserci un nuovo riferimento assegnato per ciascun object. Sembra che se il garbage collector è lento, allora avrai una serie di riferimenti penzolanti che devono essere ripuliti.

Tuttavia, se hai questo:

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

Quindi creerai solo un riferimento singolo e assegnerai un nuovo object ad esso ogni volta. Certo, potrebbe volerci un po ‘di tempo prima che esca dal campo di applicazione, ma c’è solo un riferimento ciondolante da affrontare.

Penso che dipenda dal compilatore ed è difficile dare una risposta generale.

La mia pratica è la seguente:

  • se il tipo di variabile è semplice (int, double, …) preferisco la variante b (inside).
    Motivo: riduzione dell’ambito della variabile.

  • se il tipo di variabile non è semplice (qualche tipo di class o struct ) preferisco la variante a (all’esterno).
    Motivo: riduzione del numero di chiamate di ctor-dtor.

Dal punto di vista delle prestazioni, l’esterno è (molto) migliore.

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

Ho eseguito entrambe le funzioni 1 miliardo di volte ciascuna. fuori () ha richiesto 65 millisecondi. dentro () ha impiegato 1,5 secondi.

A) è una scommessa sicura di B) ……… Immagina se stai inizializzando la struttura in loop piuttosto che ‘int’ o ‘float’ e allora?

piace

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

Sei certamente obbligato ad affrontare problemi con perdite di memoria !. Quindi credo che “A” sia una scommessa più sicura mentre “B” è vulnerabile all’accumulo di memoria esp lavorando vicino a librerie di fonti. Puoi controllare lo strumento “Valgrind” su Linux in modo specifico sotto lo strumento “Helgrind”.

È una domanda interessante Dalla mia esperienza, c’è una domanda finale da considerare quando si discute di questo argomento per un codice:

C’è qualche ragione per cui la variabile dovrebbe essere globale?

Ha senso dichiarare la variabile solo una volta, globalmente, a differenza di molte volte localmente, perché è meglio per l’organizzazione del codice e richiede meno linee di codice. Tuttavia, se deve essere dichiarato solo localmente in un metodo, lo inizializzo in quel metodo, quindi è chiaro che la variabile è esclusivamente rilevante per quel metodo. Fai attenzione a non chiamare questa variabile al di fuori del metodo in cui è inizializzata se scegli la seconda opzione: il tuo codice non saprà di cosa stai parlando e segnalerà un errore.

Inoltre, come nota a margine, non duplicare i nomi delle variabili locali tra metodi diversi anche se i loro scopi sono quasi identici; diventa solo confuso.

Ho provato per JS con il nodo 4.0.0 se qualcuno fosse interessato. La dichiarazione al di fuori dell’anello ha comportato un miglioramento delle prestazioni di ~ .5 ms in media su oltre 1000 prove con 100 milioni di iterazioni di loop per prova. Quindi dirò di andare avanti e scriverlo nel modo più leggibile / gestibile che sia B, imo. Metterei il mio codice su un violino, ma ho usato il modulo Node performance-now. Ecco il codice:

 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) 

questa è la forma migliore

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

1) in questo modo ha dichiarato una volta il tempo sia variabile che non ogni per ciclo. 2) il compito è fatser thean tutte le altre opzioni. 3) Quindi la regola bestpractice è qualsiasi dichiarazione al di fuori dell'iterazione per.

Ho provato la stessa cosa in Go e ho confrontato l’output del compilatore usando go tool compile -S con go 1.9.4

Differenza zero, in base all’output dell’assembler.

Anche se so che il mio compilatore è abbastanza intelligente, non mi piacerebbe fare affidamento su di esso, e userò la a) variante.

La b) variante ha senso per me solo se hai un disperato bisogno di rendere non disponibile il intermediateResult dopo il corpo del loop. Ma non riesco a immaginare una situazione così disperata, comunque ….

EDIT: Jon Skeet ha fatto un ottimo punto, mostrando che la dichiarazione delle variabili all’interno di un loop può fare una vera differenza semantica.