Frage

Ich habe immer gefragt, ob in der Regel einen Wegwerf-Variable vor einer Schleife deklarieren, im Gegensatz zu wiederholt innerhalb der Schleife, mache jeden (Performance) Unterschied? A (ziemlich sinnlos) Beispiel in Java:

a) Erklärung vor Schleife:

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

b) Deklaration (mehrmals), Looping:

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

Welche ist besser, a oder b ?

Ich vermute, dass die wiederholte Deklaration von Variablen (zB b ) mehr Overhead erzeugt in Theorie , aber das Compiler intelligent genug ist, so dass es keine Rolle spielt. Beispiel b hat den Vorteil, daß sie kompakter ist und den Umfang der Größenbegrenzung, wo sie verwendet wird. Dennoch neige ich dazu, nach Beispiel zu codieren a .

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

War es hilfreich?

Lösung

Was ist besser, a oder b

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

Von einer Wartung Perspektive b ist besser. Deklarieren und initialisieren Variablen in der gleichen Stelle, im engstenen Umfang möglich. Verwenden Sie keine klaffende Loch zwischen der Erklärung verlassen und der Initialisierung und nicht verschmutzen Namespaces Sie nicht brauchen.

Andere Tipps

Nun lief ich Ihre A und B Beispiele 20 mal jeweils, Looping 100 Millionen Mal (JVM - 1.5.0).

A: durchschnittliche Ausführungszeit: 0,074 s

B: durchschnittliche Ausführungszeit: 0,067 s

Zu meiner Überraschung B war etwas schneller. So schnell wie Computer jetzt seine schwer zu sagen, wenn man genau diese Maßnahme könnte. Ich würde es die A-Code Art und Weise, wie gut, aber ich würde sagen, es ist nicht wirklich wichtig.

Es hängt von der Sprache und der genauen Verwendung. Zum Beispiel 1 in C # machte es keinen Unterschied. In C # 2, wenn die lokalen Variable von einer anonymen Methode (oder Lambda-Ausdruck in C # 3) eingefangen wird, kann es einen sehr signficant Unterschied machen.

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, die gleiche outer Variable zu erfassen, aber jeder hat seinen eigenen separaten inner Variable.

Hier finden Sie, was ich schrieb und in .NET erstellt.

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

Das ist, was ich von .NET Reflector wenn CIL zurück in Code generiert.

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 genau gleiche nach der Kompilierung. In verwalteten Sprachen-Code wird in CL / Byte-Code umgewandelt und zum Zeitpunkt der Ausführung ist es in Maschinensprache umgesetzt. So in Maschinensprache nicht einmal ein Doppel auf dem Stapel erstellt werden. Es kann nur ein Register sein, wie Code widerzuspiegeln, dass es sich um eine temporäre Variable für WriteLine Funktion ist. Es gibt eine ganze Reihe Optimierungsregeln nur für Schleifen. So ist der durchschnittliche Mann sollte keine Sorgen darüber, vor allem in verwalteten Sprachen. Es gibt Fälle, wenn Sie Code verwalten optimieren können, zum Beispiel, wenn Sie eine große Anzahl von Strings verketten haben mit nur string a; a+=anotherstring[i] vs StringBuilder verwenden. Es gibt sehr großen Unterschied in der Leistung zwischen den beiden. Es gibt viele solche Fälle, in denen der Compiler den Code nicht optimieren kann, weil sie nicht herausfinden können, was in einem größeren Umfang vorgesehen ist. Aber es kann so ziemlich grundlegende Dinge optimieren für Sie.

Dies ist ein Gotcha in VB.NET. Das Visual Basic-Ergebnis wird nicht die Variable in diesem Beispiel erneut initialisieren:

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

' Output: 0 1 2 3 4...

Dieses 0 das erste Mal (Visual Basic-Variablen haben Standardwerte, wenn deklariert!) Gedruckt wird, aber i jeder Zeit danach.

Wenn Sie eine = 0 hinzufügen, aber man bekommt, was man erwarten könnte:

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

'Output: 0 0 0 0 0...

Ich habe einen einfachen Test:

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

vs

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

ich diese Codes mit gcc - 5.2.0. Und dann zerlegt ich die main () diese beiden Codes und das ist das Ergebnis:

1e:

   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

2e

   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 exaclty das gleiche asm Ergebnis. kein Beweis dafür ist, dass die beiden Codes das gleiche produzieren?

Ich würde immer A verwende (und nicht auf dem Compiler angewiesen) und möglicherweise auch neu schreiben zu:

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

Dies schränkt noch intermediateResult auf den Anwendungsbereich der Schleife, aber nicht während jeder Iteration neu deklarieren.

Es ist sprachabhängig -. IIRC C # diese optimiert, so gibt es keinen Unterschied, aber JavaScript (zum Beispiel) wird die gesamte Speicherzuweisung shebang jedes Mal tun

Meiner Meinung nach, b ist die bessere Struktur. In einem, klebt der letzte Wert von intermediateResult um nach der Schleife beendet ist.

Edit: Dies gilt nicht viel Unterschied mit Werttypen, aber Referenztypen können etwas gewichtig sein. Ich persönlich mag Variablen für die Bereinigung dereferenced so schnell wie möglich sein, und b tut das für Sie,

Ich vermute, dass einige Compiler beide optimieren könnte der gleiche Code sein, aber sicherlich nicht alle. Also würde ich sagen, dass Sie besser sind mit dem ehemaligen ab. Der einzige Grund für die letztere ist, wenn Sie, dass die deklarierte Variable sicherstellen wollen, wird nur verwendet, in der Schleife.

Als allgemeine Regel gilt, erkläre ich meine Variablen in dem Innen möglichst Umfang. Also, wenn Sie nicht außerhalb der Schleife mit intermediateResult, dann würde ich mit B gehen.

Ein Mitarbeiter zieht es die erste Form, erzählt es eine Optimierung ist, lieber eine Erklärung wieder verwenden.

ich die zweite vorziehen (! Und versuchen, meine Mitarbeiter zu überzeugen ;-)), gelesen zu haben, dass:

  • Es reduziert Umfang von Variablen, wo sie gebraucht werden, was eine gute Sache ist.
  • Java optimiert genug keinen signifikanten Unterschied in der Leistung zu machen. IIRC, vielleicht die zweite Form ist sogar noch schneller.

Wie auch immer, es fällt in die Kategorie der vorzeitigen Optimierung, die in der Qualität der Compiler angewiesen und / oder JVM.

Es gibt einen Unterschied in C #, wenn Sie die Variable in einem Lambda verwenden, etc. Aber im Allgemeinen wird den Compiler die gleiche Sache im Grunde tun, die Variable unter der Annahme, nur innerhalb der Schleife verwendet wird.

Da sie im Grunde die gleichen sind: Beachten Sie, dass Version b macht es viel offensichtlicher an die Leser, dass die Variable nicht der Fall, und kann nicht, nach der Schleife verwendet werden. Zusätzlich Version b viel mehr leicht Refactoring. Es ist schwieriger, eine der Schleifenkörper in seine eigene Methode in Version zu extrahieren. Außerdem Version b versichert Ihnen, dass es keine Nebenwirkung einer solchen Refactoring ist.

Daher ärgert Version ein mich ohne Ende, denn es gibt keinen Nutzen für sie ist, und es macht es viel schwieriger, über den Code zu folgern ...

Nun, man kann immer einen Umfang machen, dass:

{ //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 kann nur die Variable einmal erklären, und es wird sterben, wenn Sie die Schleife verlassen.

Ich habe immer gedacht, dass, wenn Sie Ihre Variablen innerhalb Ihrer Schleife deklarieren dann Speicher Sie verschwenden. Wenn Sie so etwas wie diese:

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

Dann wird nicht nur das Objekt muß für jede Iteration erstellt werden, aber es muss eine neue Referenz für jedes Objekt zugewiesen werden. Es scheint, dass, wenn der Garbage Collector langsam ist, dann werden Sie eine Reihe von baumelnden Referenzen haben, die bereinigt werden müssen.

Wenn Sie jedoch haben diese:

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

Dann sind Sie nur eine einzige Referenz zu schaffen und ein neues Objekt, um es jedes Mal zuweisen. Sicher, es könnte etwas länger dauern, bis sie den Gültigkeitsbereich zu gehen, aber dann gibt es nur einen baumelnden Bezug zu behandeln.

Ich denke, es ist an dem Compiler hängt und ist schwer, eine allgemeine Antwort zu geben.

Meine Praxis ist folgende:

  • , wenn Variablentyp ist einfach (int, double, ...) Ich ziehe Variante b (innen).
    Begründung: Rahmen variabler reduzieren.

  • , wenn Variablentyp ist nicht einfach (eine Art class oder struct) Ich ziehe Variante a (außen).
    . Grund: reduziert Anzahl der Ctor-dtor Anrufe

Aus Performance-Sicht außen ist (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;
    }
}

I ausgeführt beiden Funktionen 1 Milliarde mal jedem. nahm außerhalb () 65 Millisekunden. in () dauerte 1,5 Sekunden.

A) ist eine sichere Wette, als B) ......... Stellen Sie sich vor, wenn Sie Struktur in Schleife initialisiert werden und nicht als 'int' oder 'float' was dann?

wie

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 sind sicher gebunden Probleme mit Speicherlecks Gesicht !. Daher glaube ich, ‚A‘ sicherere Wette ist, während ‚B‘ in dem Speicher Ansammlung anfällig ist esp Arbeiten in der Nähe Quelle libraries.You kann usinng ‚Valgrind‘ Tool auf Linux überprüft speziell Sub-Tool ‚Helgrind‘.

Es ist eine interessante Frage. Aus meiner Erfahrung gibt es eine entscheidende Frage zu berücksichtigen, wenn Sie diese Angelegenheit für einen Code Debatte:

Gibt es einen Grund, warum die Variablen global sein müßten?

Es ist sinnvoll, nur die Variable einmal zu erklären, global, lokal zu oft dagegen, weil sie den Code für die Organisation besser und erfordert weniger Code-Zeilen. Wenn es jedoch nur innerhalb einer Methode lokal deklariert werden muss, würde ich es in diesem Verfahren initialisieren, so ist es klar, dass die Variable in diesem Verfahren ausschließlich relevant ist. Achten Sie darauf, diese Variable nicht außerhalb der Methode aufrufen, in dem es initialisiert wird, wenn Sie die letzte Option wählen -. Ihr Code nicht wissen, was Sie reden und wird einen Fehler melden

Auch als eine Randnotiz, duplizieren nicht lokale Variablennamen zwischen verschiedenen Methoden, auch wenn ihre Zwecke sind nahezu identisch; es wird nur verwirrend.

Ich testete für JS mit Knoten 4.0.0, wenn jemand interessiert ist. in einer ~ 0,5 ms Leistungsverbesserung geführt außerhalb der Schleife Deklarieren im Durchschnitt mehr als 1000 Studien mit 100 Millionen Schleifendurchläufen pro Versuch. Also, ich werde sagen, gehen Sie vor und schreiben Sie es in den meisten lesbar / wartbar Weg die B ist, imo. Ich würde meinen Code in einer Geige setzen, aber ich verwenden, um das Performance-jetzt Node-Modul. 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)

Dies 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 Zeit sowohl Variable deklariert, und nicht jeder für Zyklus. 2) die Zuordnung es fatser thean alle anderen Option. 3) So ist die Best-Practice-Regel ist jede Erklärung außerhalb der Iteration für.

das gleiche in Go versucht, und der Compiler Ausgabe verglichen mit go tool compile -S mit gehen 1.9.4

Null Unterschied, wie pro Assembler ausgegeben.

Ich hatte diese sehr gleiche Frage für eine lange Zeit. So testete ich ein noch einfacheres Stück Code.

Fazit:. solche Fälle ist NO Performance-Unterschied

Außenlooping Fall

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

Looping Fall

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

überprüfte ich die kompilierte Datei auf IntelliJ der Decompiler und für beide Fälle, bekam ich die gleichen Test.class

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

ich auch beiden Codes für den in diesem beantworten gegeben unter Verwendung des Verfahrens Fall zerlegt. Ich zeige nur die Teile, die für die Antwort

Außenlooping Fall

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;

Looping Fall

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 darauf achten, nur die Slot zugewiesen i und intermediateResult in LocalVariableTable als ein Produkt von der Reihenfolge ihres Auftretens getauscht. Der gleiche Unterschied in Schlitz ist in anderen Zeilen Code reflektiert wird.

  • kein zusätzlicher Vorgang durchgeführt wird
  • intermediateResult ist immer noch eine lokale Variable in beiden Fällen, so gibt es keinen Unterschied Zugriffszeit.

BONUS

Compiler eine Tonne Optimierung tun, werfen Sie einen Blick auf das, was in diesem Fall passiert.

Nullarbeits Fall

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

Null Arbeit dekompilierten

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

Auch wenn ich weiß, dass mein Compiler intelligent genug ist, werde ich nicht darauf gerne verlassen, und die a) Variante verwenden.

Die b) Variante macht Sinn für mich nur, wenn Sie verzweifelt die intermediateResult nicht verfügbar, nachdem die Schleife zu machen braucht. Aber ich kann nicht so verzweifelte Situation vorstellen, sowieso ....

EDIT:. Jon Skeet einen sehr guten Punkt gemacht, die zeigt, dass Variablendeklaration innerhalb einer Schleife kann einen tatsächlichen semantischen Unterschied machen

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top