web-dev-qa-db-de.com

Unterschied zwischen der Deklaration von Variablen vor oder in der Schleife?

Ich habe mich immer gefragt, ob das Deklarieren einer Wegwerfvariablen vor einer Schleife im Gegensatz zu wiederholt innerhalb der Schleife einen (Performance-) Unterschied ausmacht. Ein (ziemlich sinnlos) Beispiel in Java:

a) Deklaration vor der Schleife:

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

b) Deklaration (wiederholt) innerhalb der Schleife:

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

Welcher ist besser, a oder b

Ich vermute, dass die wiederholte Variablendeklaration (example b ) mehr Overhead theoretisch erzeugt, aber dass Compiler intelligent genug sind, sodass es keine Rolle spielt. Beispiel b hat den Vorteil, dass es kompakter ist und den Gültigkeitsbereich der Variablen auf den Verwendungsbereich beschränkt. Dennoch neige ich dazu, nach Beispiel a zu codieren.

Edit:Ich interessiere mich besonders für den Java-Fall.

299
Rabarberski

Was ist besser, a oder b ?

Aus Performance-Sicht müssten Sie es messen. (Und meiner Meinung nach ist der Compiler, wenn Sie einen Unterschied messen können, nicht sehr gut).

Aus Wartungssicht ist b besser. Deklarieren und initialisieren Sie Variablen an der gleichen Stelle, im engsten möglichen Bereich. Lassen Sie keine Diskrepanz zwischen Deklaration und Initialisierung und verschmutzen Sie keine Namespaces, die Sie nicht benötigen.

244

Nun, ich habe Ihre A- und B-Beispiele jeweils 20-mal ausgeführt und eine Schleife von 100 Millionen Mal ausgeführt. (JVM - 1.5.0)

A: durchschnittliche Ausführungszeit: 0,074 Sekunden

B: durchschnittliche Ausführungszeit: 0,067 Sekunden

Zu meiner Überraschung war B etwas schneller .. So schnell wie Computer jetzt sind, ist es schwer zu sagen, ob Sie das genau messen könnten.A. Ich würde es auch als A-Weg kodieren, aber ich würde sagen, es spielt keine Rolle .

207
Mark

Das hängt von der Sprache und der genauen Verwendung ab. Zum Beispiel machte es in C # 1 keinen Unterschied. Wenn in C # 2 die lokale Variable von einer anonymen Methode (oder einem Lambda-Ausdruck in C # 3) erfasst wird, kann dies einen erheblichen Unterschied ausmachen.

Beispiel:

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

Ausgabe:

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

Der Unterschied besteht darin, dass alle Aktionen dieselbe Variable outer erfassen, aber jede hat ihre eigene Variable inner.

66
Jon Skeet

Folgendes habe ich in .NET geschrieben und kompiliert.

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

Dies ist, was ich von .NET Reflector bekomme, wenn CIL wieder in Code gerendert wird.

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

So sehen beide nach der Kompilierung genau gleich aus. In verwalteten Sprachen wird Code in CL/Byte-Code konvertiert und zum Zeitpunkt der Ausführung in Maschinensprache konvertiert. In Maschinensprache wird also möglicherweise nicht einmal ein Double auf dem Stack erstellt. Es kann nur ein Register sein, da der Code darauf hinweist, dass es sich um eine temporäre Variable für die WriteLine-Funktion handelt. Es gibt eine ganze Reihe von Optimierungsregeln nur für Schleifen. Der Durchschnittsmensch sollte sich deshalb keine Sorgen machen, besonders in verwalteten Sprachen. Es gibt Fälle, in denen Sie den Verwaltungscode optimieren können, z. B. wenn Sie eine große Anzahl von Zeichenfolgen mithilfe von string a; a+=anotherstring[i] vs mit StringBuilder verketten müssen. Es gibt sehr große Leistungsunterschiede zwischen beiden. In vielen Fällen kann der Compiler Ihren Code nicht optimieren, da er nicht herausfinden kann, was in einem größeren Bereich beabsichtigt ist. Aber es kann so ziemlich grundlegende Dinge für Sie optimieren.

35
particle

Dies ist ein Problem in VB.NET. Das Visual Basic-Ergebnis initialisiert die Variable in diesem Beispiel nicht erneut:

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

' Output: 0 1 2 3 4...

Beim ersten Mal wird 0 ausgegeben (Visual Basic-Variablen haben bei der Deklaration! Standardwerte), danach jedoch i.

Wenn Sie jedoch einen = 0 hinzufügen, erhalten Sie, was Sie vielleicht erwarten:

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

Ich habe einen einfachen Test gemacht:

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

vs 

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

Ich habe diese Codes mit gcc - 5.2.0 kompiliert. Und dann habe ich den main () Dieser beiden Codes demontiert und das ist das Ergebnis:

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 

Welche sind genau das gleiche als Ergebnis. Ist das nicht der Beweis, dass die beiden Codes dasselbe produzieren?

15
UserX

Ich würde immer A verwenden (anstatt sich auf den Compiler zu verlassen) und könnte auch umschreiben in:

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

Dies beschränkt intermediateResult weiterhin auf den Gültigkeitsbereich der Schleife, dekliniert jedoch nicht bei jeder Iteration neu.

11
Triptych

Es ist sprachabhängig - IIRC C # optimiert dies, es gibt also keinen Unterschied, aber JavaScript (zum Beispiel) übernimmt jedes Mal die gesamte Speicherbelegung.

11
annakata

Meiner Meinung nach ist b die bessere Struktur. In a bleibt der letzte Wert von intermediResult erhalten, nachdem Ihre Schleife beendet ist.

Edit: Das macht bei Werttypen keinen großen Unterschied, aber Referenztypen können etwas gewichtig sein. Persönlich mag ich, dass Variablen so schnell wie möglich für die Bereinigung dereferenziert werden, und b tut das für Sie,

6
Powerlord

Nun, man könnte immer einen Spielraum dafür schaffen:

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

Auf diese Weise deklarieren Sie die Variable nur einmal und sterben, wenn Sie die Schleife verlassen.

5
Marcelo Faísca

Es gibt einen Unterschied in C #, wenn Sie die Variable in einem Lambda usw. verwenden. Im Allgemeinen wird der Compiler jedoch grundsätzlich dasselbe tun, vorausgesetzt, die Variable wird nur innerhalb der Schleife verwendet. 

Sie sind im Wesentlichen gleich: Beachten Sie, dass Version b den Lesern deutlich macht, dass die Variable nach der Schleife nicht verwendet werden kann und nicht verwendet werden kann. Version b lässt sich außerdem viel einfacher umgestalten. In Version a ist es schwieriger, den Schleifenkörper in eine eigene Methode zu extrahieren. Außerdem versichert Ihnen Version b, dass ein solches Refactoring keinen Nebeneffekt hat.

Daher ärgert mich die Version a ohne Ende, denn sie bringt keinen Nutzen und macht es viel schwieriger, über den Code nachzudenken ...

5
Mark Sowul

Ein Mitarbeiter bevorzugt das erste Formular. Es wird als Optimierung bezeichnet und es wird bevorzugt, eine Deklaration erneut zu verwenden.

Ich bevorzuge den zweiten (und versuche, meinen Kollegen zu überzeugen! ;-)), nachdem ich das gelesen habe:

  • Es reduziert den Umfang der Variablen dahin, wo sie benötigt werden, was eine gute Sache ist.
  • Java optimiert genug, um die Leistung nicht wesentlich zu beeinträchtigen. IIRC, vielleicht ist die zweite Form noch schneller.

Jedenfalls fällt es in die Kategorie der vorzeitigen Optimierung, die auf die Qualität des Compilers und/oder der JVM angewiesen ist.

5
PhiLho

Ich vermute, ein paar Compiler könnten den gleichen Code optimieren, aber sicher nicht alle. Ich würde also sagen, Sie sind besser mit dem ehemaligen. Der einzige Grund für letzteres ist, wenn Sie sicherstellen möchten, dass die deklarierte Variable only in Ihrer Schleife verwendet wird.

5
Stew S

In der Regel erkläre ich meine Variablen im innersten Bereich. Wenn Sie also intermediResult nicht außerhalb der Schleife verwenden, würde ich mit B gehen.

5
Chris

Ich habe immer gedacht, wenn Sie Ihre Variablen innerhalb Ihrer Schleife deklarieren, verschwenden Sie Speicherplatz. Wenn Sie so etwas haben:

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

Dann muss das Objekt nicht nur für jede Iteration erstellt werden, sondern es muss eine neue Referenz für jedes Objekt zugewiesen werden. Es scheint, dass, wenn der Garbage Collector langsam ist, eine Reihe von baumelnden Referenzen vorhanden ist, die aufgeräumt werden müssen.

Wenn Sie jedoch Folgendes haben:

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

Dann erstellen Sie nur eine einzelne Referenz und weisen ihr jedes Mal ein neues Objekt zu. Sicher, es könnte etwas länger dauern, bis es den Rahmen verlässt, aber dann gibt es nur eine einzige Referenz, mit der man sich auseinandersetzen muss.

4
R. Carr

Meine Praxis ist folgende: 

  • wenn der Typ der Variablen einfach ist (int, double, ...) Ich bevorzuge die Variante b (inside).
    Grund: Reduziert den Umfang der Variablen. 

  • wenn der Variablentyp nicht einfach ist (eine Art von class oder struct) Ich bevorzuge die Variante a (außerhalb).
    Grund: Verringerung der Anzahl von ctor-dtor-Aufrufen.

3
fat

Ich denke, das hängt vom Compiler ab und ist schwer zu beantworten.

3
SquidScareMe

Aus Sicht der Leistung ist draußen (viel) besser.

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

Ich habe beide Funktionen jeweils 1 Milliarde mal ausgeführt. Outside () dauerte 65 Millisekunden. inside () dauerte 1,5 Sekunden.

1
Alex

Das ist eine interessante Frage. Aus meiner Erfahrung gibt es eine letzte Frage, die Sie in Betracht ziehen sollten, wenn Sie diese Angelegenheit für einen Code diskutieren:

Gibt es einen Grund, warum die Variable global sein müsste?

Es ist sinnvoll, die Variable nur einmal global zu deklarieren, im Gegensatz zu vielen lokalen Vorgängen, da dies für die Organisation des Codes besser ist und weniger Codezeilen erforderlich ist. Wenn es jedoch nur innerhalb einer Methode lokal deklariert werden muss, würde ich es in dieser Methode initialisieren, sodass klar ist, dass die Variable ausschließlich für diese Methode relevant ist. Achten Sie darauf, diese Variable nicht außerhalb der Methode aufzurufen, in der sie initialisiert wird, wenn Sie die letztere Option wählen. Ihr Code weiß nicht, worüber Sie sprechen, und meldet einen Fehler.

Als Randbemerkung dürfen Sie auch keine lokalen Variablennamen zwischen verschiedenen Methoden duplizieren, auch wenn ihre Zwecke nahezu identisch sind. es wird nur verwirrend. 

0
Joshua Siktar

Habe dasselbe in Go ausprobiert und die Compilerausgabe mit go tool compile -S mit go 1.9.4 verglichen

Null Unterschied gemäß der Assembler-Ausgabe.

0
SteveOC 64

Diese Frage hatte ich lange Zeit. Also habe ich ein noch einfacheres Stück Code getestet. 

Fazit: Für solche Fälle gibt es KEINE Leistungsunterschiede.

Außerhalb der Schleife

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

Inside Schleife

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

Ich habe die kompilierte Datei im IntelliJ-Decompiler geprüft und für beide Fälle das sameTest.class erhalten.

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

Ich habe auch den Code für beide Fälle mit der in dieser Antwort angegebenen Methode zerlegt. Ich zeige nur die für die Antwort relevanten Teile

Außerhalb der Schleife

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;

Inside Schleife

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;

Wenn Sie genau aufpassen, wird nur die Slot, die i und intermediateResult in LocalVariableTable zugewiesen ist, als Produkt ihrer Erscheinungsreihenfolge vertauscht. Der gleiche Unterschied in den Slots spiegelt sich auch in anderen Codezeilen wider.

  • Es wird keine zusätzliche Operation ausgeführt
  • intermediateResult ist in beiden Fällen immer noch eine lokale Variable, daher gibt es keine Differenzzugriffszeit.

BONUS 

Compiler optimieren jede Menge, schauen Sie sich an, was in diesem Fall passiert.

Nullarbeitsfall

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

Nullarbeit dekompiliert

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

das ist die bessere Form

double intermediateResult;
int i = byte.MinValue;

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

1) Auf diese Weise einmal deklariert beide Variablen, und nicht jede für den Zyklus . 2) Die Zuweisung ist fetterer als alle anderen Optionen . 3) Die beste Praxisregel ist daher jede Deklaration außerhalb der Iteration.

0
luka

A) ist eine sichere Sache als B) ......... Stellen Sie sich vor, wenn Sie die Struktur in einer Schleife initialisieren und nicht 'int' oder 'float', was dann?

mögen 

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

Sie haben sicherlich Probleme mit Speicherlecks! Daher glaube ich, dass 'A' sicherer ist, während 'B' anfällig für Speicherakkumulation ist, insbesondere wenn Sie nahe Quellbibliotheken arbeiten. 

0

Ich habe JS mit Node 4.0.0 getestet, wenn jemand interessiert ist. Die Deklaration außerhalb der Schleife führte zu einer Leistungsverbesserung von ~ 0,5 ms im Durchschnitt über 1000 Studien mit 100 Millionen Schleifeniterationen pro Studie. Also werde ich sagen, gehen Sie vor und schreiben Sie es auf die am besten lesbare/wartbare Weise, die B, imo ist. Ich würde meinen Code in eine Geige stecken, aber ich habe das Modul "Performance-Now Node" verwendet. Hier ist der 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