Domanda

Mi sono sempre chiesto se, in generale, dichiarare una variabile usa e getta prima di un loop, anziché ripetutamente all'interno del loop, faccia qualche differenza (di 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 ciclo:

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 più in teoria , ma che i compilatori siano abbastanza intelligenti da non importare. L'esempio b ha il vantaggio di essere più compatto e di limitare l'ambito della variabile a dove viene utilizzata. Tendo comunque a codificare secondo l'esempio a .

Modifica : Sono particolarmente interessato al caso Java.

È stato utile?

Soluzione

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, nell'ambito più ristretto possibile. Non lasciare un buco tra la dichiarazione e l'inizializzazione e non inquinare gli spazi dei nomi che non è necessario.

Altri suggerimenti

Beh, ho eseguito i tuoi esempi A e B 20 volte ciascuno, ripetendo 100 milioni di volte. (JVM - 1.5.0)

A: tempo medio di esecuzione: 0,074 secondi

B: tempo medio di esecuzione: 0,067 sec

Con mia sorpresa B era leggermente più veloce. Per quanto i computer siano ormai difficili, è difficile dire se è possibile misurarli con precisione. Lo codificherei anche nel modo A, ma direi che non importa davvero.

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

Output:

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 esterna , ma ognuna ha la propria variabile interna separata.

Quello che segue è quello 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 riportato nel 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 uguali dopo la compilazione. Nelle lingue gestite il codice viene convertito in codice CL / byte e al momento dell'esecuzione viene convertito in linguaggio macchina. Quindi, in linguaggio macchina, un doppio non può nemmeno essere creato nello stack. 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. Ci sono casi in cui puoi ottimizzare la gestione del codice, ad esempio, se devi concatenare un gran numero di stringhe usando solo stringa a; a + = anotherstring [i] vs usando StringBuilder . C'è una grande differenza nelle prestazioni tra i due. 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 gotcha in VB.NET. Il risultato di Visual Basic non reinizializza 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...

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

Se aggiungi un = 0 , ottieni quello 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 <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 

Quali sono esattamente lo stesso risultato. non è una prova che i due codici producano 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 intermedioResult all'ambito del ciclo, ma non viene redeclarecato durante ogni iterazione.

Dipende 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 intermedioRisult rimane attivo al termine del ciclo.

Modifica: Questo non fa molta differenza con i tipi di valore, ma i tipi di riferimento possono essere piuttosto pesanti. Personalmente, mi piace che le variabili vengano dereferenziate il prima possibile 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'unica ragione per quest'ultima è se vuoi assicurarti che la variabile dichiarata sia usata solo all'interno del tuo ciclo.

Come regola generale, dichiaro le mie variabili nell'ambito più interno possibile. Quindi, se non stai usando intermedioResult al di fuori del ciclo, allora andrei con B.

Un collega preferisce il primo modulo, dicendo che si tratta di un'ottimizzazione, preferendo riutilizzare una dichiarazione.

Preferisco il secondo (e provo a convincere il mio collega! ;-)), dopo aver letto che:

  • Riduce la portata delle variabili nel punto in cui sono necessarie, il che è positivo.
  • 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 dell'ottimizzazione prematura che si basa sulla qualità del compilatore e / o JVM.

C'è una differenza in C # se stai usando la variabile in una lambda, ecc. Ma in generale il compilatore farà sostanzialmente la stessa cosa, supponendo che la variabile sia usata solo all'interno del ciclo.

Dato che sono sostanzialmente gli stessi: nota che la versione b rende molto più ovvio ai lettori che la variabile non è, e non può, essere usata dopo il ciclo. Inoltre, la versione b è molto più facilmente riformulata. È più difficile estrarre il corpo del loop nel suo metodo nella versione a. Inoltre, la versione b ti assicura che non vi sono effetti collaterali in un tale refactoring.

Quindi, la versione a mi infastidisce senza fine, perché non ha alcun vantaggio e rende molto più difficile ragionare sul codice ...

Beh, potresti 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 dichiari la variabile una sola volta e morirà quando lasci 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'oggetto deve essere creato per ogni iterazione, ma deve essere assegnato un nuovo riferimento per ogni oggetto. Sembra che se il garbage collector è lento, avrai un sacco di riferimenti penzolanti che devono essere ripuliti.

Tuttavia, se hai questo:

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

Quindi stai solo creando un singolo riferimento e assegnandogli un nuovo oggetto ogni volta. Certo, potrebbe volerci un po 'più tempo prima che esca dal campo di applicazione, ma poi c'è solo un riferimento penzolante 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 (all'interno).
    Motivo: riducendo l'ambito della variabile.

  • se il tipo di variabile non è semplice (una sorta di classe o struct ) preferisco la variante a (esterno).
    Motivo: riduzione del numero di chiamate 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. outside () ha impiegato 65 millisecondi. inside () 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?

come

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 sicuramente tenuto ad affrontare problemi con perdite di memoria !. Quindi credo che 'A' sia una scommessa più sicura mentre 'B' è vulnerabile all'accumulo di memoria, specialmente nelle librerie di sorgenti vicine. Puoi controllare usando lo strumento 'Valgrind' su Linux, in particolare lo strumento secondario 'Helgrind'.

È una domanda interessante. Dalla mia esperienza c'è un'ultima domanda da considerare quando si discute di questo argomento per un codice:

C'è qualche motivo per cui la variabile dovrebbe essere globale?

Ha senso dichiarare la variabile una sola volta, a livello globale, anziché molte volte localmente, perché è meglio per organizzare il codice e richiede meno righe di codice. Tuttavia, se deve essere dichiarato solo localmente all'interno di un metodo, lo inizializzerei in quel metodo, quindi è chiaro che la variabile è pertinente esclusivamente a quel metodo. Fai attenzione a non chiamare questa variabile al di fuori del metodo in cui è inizializzata se scegli quest'ultima 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 confuso.

Ho testato per JS con il nodo 4.0.0 se qualcuno è interessato. La dichiarazione al di fuori del ciclo ha comportato un miglioramento delle prestazioni di ~ 0,5 ms in media oltre 1000 prove con 100 milioni di iterazioni di ciclo per prova. Quindi dirò di andare avanti e scriverlo nel modo più leggibile / gestibile che è B, imo. Vorrei mettere il mio codice in un violino, ma ho usato il modulo Node di 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 dichiarato una volta entrambe le variabili, e non ognuna per ciclo. 2) il compito è più grasso di tutte le altre opzioni. 3) Quindi la regola di bestpractice è qualsiasi dichiarazione al di fuori dell'iterazione per.

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

Differenza zero, secondo l'output dell'assemblatore.

Ho avuto questa stessa domanda per molto tempo. Quindi ho testato un codice ancora più semplice.

Conclusione: per casi del genere c'è NO differenza di prestazioni.

Custodia per anello esterno

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

Custodia per anello interno

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

Ho controllato il file compilato sul decompilatore di IntelliJ e in entrambi i casi ho ottenuto lo stesso Test.class

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

Ho anche smontato il codice per entrambi i casi utilizzando il metodo indicato in questa risposta . Mostrerò solo le parti pertinenti alla risposta

Custodia per anello esterno

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;

Custodia per anello interno

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;

Se presti molta attenzione, solo lo Slot assegnato a i e intermediaResult in LocalVariableTable viene scambiato come un prodotto del loro ordine di apparizione. La stessa differenza nello slot si riflette in altre righe di codice.

  • Non viene eseguita alcuna operazione aggiuntiva
  • intermedioResult è ancora una variabile locale in entrambi i casi, quindi non vi è alcuna differenza nel tempo di accesso.

BONUS

I compilatori fanno un sacco di ottimizzazione, dai un'occhiata a cosa succede in questo caso.

Caso di lavoro zero

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

Zero lavori decompilati

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

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

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

EDIT: Jon Skeet ha fatto un'ottima osservazione, dimostrando che la dichiarazione delle variabili all'interno di un loop può fare una differenza semantica effettiva.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top