Gibt es einen Overhead beim Deklarieren einer Variablen innerhalb einer Schleife? (C ++)

157

Ich frage mich nur, ob es zu einem Geschwindigkeits- oder Effizienzverlust kommen würde, wenn Sie so etwas tun würden:

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

das erklärt int varhundertmal. Es scheint mir, als würde es das geben, aber ich bin mir nicht sicher. Wäre es praktischer / schneller, dies stattdessen zu tun:

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

oder sind sie gleich, schnell und effizient?


quelle
7
Um klar zu sein, deklariert der obige Code var nicht hundertmal.
Jason
1
@ Rabarberski: Die referenzierte Frage ist kein genaues Duplikat, da sie keine Sprache angibt. Diese Frage ist spezifisch für C ++ . Entsprechend den Antworten auf Ihre Frage, auf die verwiesen wird, hängt die Antwort jedoch von der Sprache und möglicherweise vom Compiler ab.
DavidRR
2
@jason Wenn der erste Codeausschnitt die Variable 'var' nicht hundertmal deklariert, können Sie erklären, was passiert? Deklariert es die Variable nur einmal und initialisiert sie 100 Mal? Ich hätte gedacht, dass der Code die Variable 100 Mal deklariert und initialisiert, da alles in der Schleife 100 Mal ausgeführt wird. Vielen Dank.
randomUser47534

Antworten:

193

Der Stapelspeicher für lokale Variablen wird normalerweise im Funktionsumfang zugewiesen. Innerhalb der Schleife findet also keine Stapelzeigeranpassung statt, sondern nur 4 zugewiesen var. Daher haben diese beiden Snippets den gleichen Overhead.

laalto
quelle
50
Ich wünschte, die Leute, die am College unterrichten, wüssten zumindest diese grundlegende Sache. Einmal lachte er mich aus, als ich eine Variable in einer Schleife deklarierte, und ich fragte mich, was los war, bis er die Leistung als Grund dafür anführte und ich wie "WTF!?" War.
Mehrdad Afshari
18
Sind Sie sicher, dass Sie sofort über Stapelspeicher sprechen sollten? Eine solche Variable könnte sich auch in einem Register befinden.
toto
3
@toto Eine Variable wie diese könnte auch nirgendwo sein - die varVariable wird initialisiert, aber nie verwendet, sodass ein vernünftiger Optimierer sie einfach vollständig entfernen kann (mit Ausnahme des zweiten Snippets, wenn die Variable irgendwo nach der Schleife verwendet wurde).
CiaPan
@Mehrdad Afshari Eine Variable in einer Schleife erhält ihren Konstruktor einmal pro Iteration aufgerufen. BEARBEITEN - Ich sehe, dass Sie dies unten erwähnt haben, aber ich denke, dass es auch in der akzeptierten Antwort Erwähnung verdient.
Hoodaticus
105

Bei primitiven Typen und POD-Typen spielt dies keine Rolle. Der Compiler weist den Stapelspeicherplatz für die Variable zu Beginn der Funktion zu und gibt die Zuordnung frei, wenn die Funktion in beiden Fällen zurückkehrt.

Bei Nicht-POD-Klassentypen mit nicht trivialen Konstruktoren wird dies einen Unterschied machen. In diesem Fall ruft das Platzieren der Variablen außerhalb der Schleife den Konstruktor und den Destruktor nur einmal und den Zuweisungsoperator bei jeder Iteration auf, während sie innerhalb der Iteration platziert wird loop ruft den Konstruktor und den Destruktor für jede Iteration der Schleife auf. Abhängig davon, was der Konstruktor, der Destruktor und der Zuweisungsoperator der Klasse tun, kann dies wünschenswert sein oder auch nicht.

Adam Rosenfield
quelle
41
Richtige Idee falscher Grund. Variable außerhalb der Schleife. Einmal konstruiert, einmal zerstört, aber der Zuweisungsoperator hat jede Iteration angewendet. Variable innerhalb der Schleife. Konstrukt / Desatruktor hat jede Iteration angewendet, jedoch keine Zuweisungsoperationen.
Martin York
8
Dies ist die beste Antwort, aber diese Kommentare sind verwirrend. Es gibt einen großen Unterschied zwischen dem Aufrufen eines Konstruktors und eines Zuweisungsoperators.
Andrew Grant
1
Es ist wahr, wenn der Schleifenkörper die Zuweisung trotzdem ausführt, nicht nur zur Initialisierung. Und wenn es nur eine körperunabhängige / konstante Initialisierung gibt, kann der Optimierer sie hochziehen.
Peterchen
7
@ Andrew Grant: Warum. Der Zuweisungsoperator wird normalerweise als Kopierkonstrukt in tmp definiert, gefolgt von Swap (um ausnahmsicher zu sein) und anschließendem Zerstören von tmp. Somit unterscheidet sich der Zuweisungsoperator nicht wesentlich vom obigen Konstruktions- / Zerstörungszyklus. Ein Beispiel für einen typischen Zuweisungsoperator finden Sie unter stackoverflow.com/questions/255612/… .
Martin York
1
Wenn Konstrukt / Zerstörung teuer sind, sind ihre Gesamtkosten eine angemessene Obergrenze für die Kosten des Betreibers =. Aber die Aufgabe könnte tatsächlich billiger sein. Wenn wir diese Diskussion von Ints auf C ++ - Typen erweitern, könnte man 'var = 4' als eine andere Operation als 'Variable aus einem Wert desselben Typs zuweisen' verallgemeinern.
Greggo
68

Sie sind beide gleich, und wie Sie dies herausfinden können, indem Sie sich ansehen, was der Compiler tut (auch ohne auf hoch eingestellte Optimierung):

Schauen Sie sich an, was der Compiler (gcc 4.0) mit Ihren einfachen Beispielen macht:

1.c:

main(){ int var; while(int i < 100) { var = 4; } }

gcc -S 1.c

1.s:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2.c

main() { while(int i < 100) { int var = 4; } }

gcc -S 2.c

2.s:

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

Daraus können Sie zwei Dinge erkennen: Erstens ist der Code in beiden Fällen gleich.

Zweitens wird der Speicher für var außerhalb der Schleife zugewiesen:

         subl    $24, %esp

Und schließlich ist das einzige, was in der Schleife steht, die Zuordnung und Bedingungsprüfung:

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

Das ist ungefähr so ​​effizient wie möglich, ohne die Schleife vollständig zu entfernen.

Alex Brown
quelle
2
"Was ungefähr so ​​effizient ist, wie Sie sein können, ohne die Schleife vollständig zu entfernen" Nicht ganz. Das teilweise Abrollen der Schleife (etwa 4 Mal pro Durchgang) würde sie dramatisch beschleunigen. Es gibt wahrscheinlich viele andere Möglichkeiten zur Optimierung ... obwohl die meisten modernen Compiler wahrscheinlich erkennen würden, dass es überhaupt keinen Sinn macht, eine Schleife zu erstellen. Wenn 'i' später verwendet wurde, wurde einfach 'i' = 100 gesetzt.
darron
Das setzt voraus, dass der Code überhaupt in inkrementiertes 'i' geändert wurde ... da es sich nur um eine Ewigkeitsschleife handelt.
Darron
Wie war der Originalbeitrag!
Alex Brown
2
Ich mag Antworten, die die Theorie mit Beweisen untermauern! Schön zu sehen, dass ASM Dump die Theorie der Gleichheit unterstützt. +1
Xavi Montero
1
Ich habe die Ergebnisse tatsächlich erstellt, indem ich den Maschinencode für jede Version generiert habe. Keine Notwendigkeit, es auszuführen.
Alex Brown
13

Heutzutage ist es besser, es innerhalb der Schleife zu deklarieren, es sei denn, es ist eine Konstante, da der Compiler den Code besser optimieren kann (Reduzierung des Variablenbereichs).

EDIT: Diese Antwort ist derzeit größtenteils veraltet. Mit dem Aufkommen postklassischer Compiler werden die Fälle, in denen der Compiler es nicht herausfinden kann, seltener. Ich kann sie immer noch konstruieren, aber die meisten Leute würden die Konstruktion als schlechten Code klassifizieren.

Joshua
quelle
4
Ich bezweifle, dass sich dies auf die Optimierung auswirkt. Wenn der Compiler eine Datenflussanalyse durchführt, kann er feststellen, dass er nicht außerhalb der Schleife geändert wird. Daher sollte er in beiden Fällen denselben optimierten Code erzeugen.
Adam Rosenfield
3
Es wird jedoch nicht herausgefunden, ob Sie zwei verschiedene Schleifen haben, die denselben temporären Variablennamen verwenden.
Joshua
11

Die meisten modernen Compiler optimieren dies für Sie. Davon abgesehen würde ich Ihr erstes Beispiel verwenden, da ich es besser lesbar finde.

Andrew Hare
quelle
3
Ich zähle es nicht wirklich als Optimierung. Da es sich um lokale Variablen handelt, wird der Stapelspeicher nur zu Beginn der Funktion zugewiesen. Es gibt keine wirkliche "Schöpfung", die die Leistung beeinträchtigt (es sei denn, ein Konstruktor wird aufgerufen, was eine völlig andere Geschichte ist).
Mehrdad Afshari
Sie haben Recht, "Optimierung" ist das falsche Wort, aber ich bin ratlos für ein besseres.
Andrew Hare
Das Problem ist, dass ein solcher Optimierer eine Live-Bereichsanalyse verwendet und beide Variablen ziemlich tot sind.
MSalters
Wie wäre es mit "Der Compiler wird nach der Datenflussanalyse keinen Unterschied zwischen ihnen feststellen". Persönlich bevorzuge ich, dass der Umfang einer Variablen auf den Verwendungsort beschränkt wird, nicht aus Gründen der Effizienz, sondern aus Gründen der Klarheit.
Greggo
9

Bei einem integrierten Typ gibt es wahrscheinlich keinen Unterschied zwischen den beiden Stilen (wahrscheinlich bis zum generierten Code).

Wenn die Variable jedoch eine Klasse mit einem nicht trivialen Konstruktor / Destruktor ist, kann es durchaus zu einem großen Unterschied bei den Laufzeitkosten kommen. Ich würde die Variable im Allgemeinen auf den Bereich innerhalb der Schleife beschränken (um den Bereich so klein wie möglich zu halten), aber wenn sich herausstellt, dass dies einen perfekten Einfluss hat, würde ich versuchen, die Klassenvariable außerhalb des Bereichs der Schleife zu verschieben. Dies erfordert jedoch einige zusätzliche Analysen, da sich die Semantik des Odenpfads ändern kann. Dies kann daher nur erfolgen, wenn die Sematik dies zulässt.

Eine RAII-Klasse benötigt möglicherweise dieses Verhalten. Beispielsweise muss möglicherweise bei jeder Schleifeniteration eine Klasse erstellt und zerstört werden, die die Lebensdauer des Dateizugriffs verwaltet, um den Dateizugriff ordnungsgemäß zu verwalten.

Angenommen, Sie haben eine LockMgrKlasse, die beim Erstellen einen kritischen Abschnitt erfasst und bei Zerstörung freigibt:

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

ist ganz anders als:

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}
Michael Burr
quelle
6

Beide Schleifen haben den gleichen Wirkungsgrad. Sie werden beide unendlich viel Zeit in Anspruch nehmen :) Es kann eine gute Idee sein, i innerhalb der Schleifen zu erhöhen.

Larry Watanabe
quelle
Ah ja, ich habe vergessen, die Raumeffizienz anzusprechen - das ist in Ordnung - 2 Zoll für beide. Es scheint mir nur seltsam, dass Programmierern die Gesamtstruktur für den Baum fehlt - all diese Vorschläge zu Code, der nicht beendet wird.
Larry Watanabe
Es ist in Ordnung, wenn sie nicht kündigen. Keiner von ihnen wird gerufen. :-)
Nosredna
2

Ich habe einmal einige Leistungstests durchgeführt und zu meiner Überraschung festgestellt, dass Fall 1 tatsächlich schneller war! Ich nehme an, dies kann daran liegen, dass das Deklarieren der Variablen innerhalb der Schleife ihren Umfang verringert, sodass sie früher freigegeben wird. Das ist jedoch lange her, auf einem sehr alten Compiler. Ich bin mir sicher, dass moderne Compiler die Unterschiede besser optimieren können, aber es tut immer noch nicht weh, Ihren variablen Bereich so kurz wie möglich zu halten.

user3864776
quelle
Der Unterschied ergibt sich wahrscheinlich aus dem Unterschied im Umfang. Je kleiner der Bereich, desto wahrscheinlicher ist es, dass der Compiler die Serialisierung der Variablen eliminieren kann. Im Bereich der kleinen Schleife wurde die Variable wahrscheinlich in ein Register gestellt und nicht im Stapelrahmen gespeichert. Wenn Sie eine Funktion in der Schleife aufrufen oder einen Zeiger dereferenzieren, auf den der Compiler nicht wirklich weiß, wohin er zeigt, wird die Schleifenvariable verschüttet, wenn sie sich im Funktionsumfang befindet (der Zeiger enthält möglicherweise &i).
Patrick Schlüter
Bitte posten Sie Ihr Setup und Ihre Ergebnisse.
Jxramos
2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

Der obige Code gibt immer 100 10-mal aus, was bedeutet, dass die lokale Variable innerhalb der Schleife nur einmal pro Funktionsaufruf zugewiesen wird.

Byeonggon Lee
quelle
0

Der einzige Weg, um sicher zu sein, besteht darin, sie zeitlich zu bestimmen. Aber der Unterschied, wenn es einen gibt, wird mikroskopisch sein, so dass Sie eine mächtige große Zeitschleife benötigen.

Genauer gesagt ist der erste Stil besser, da er die Variable var initialisiert, während der andere sie nicht initialisiert. Dies und die Richtlinie, dass Variablen so nahe wie möglich an ihrem Verwendungsort definiert werden sollten, bedeuten, dass normalerweise die erste Form bevorzugt werden sollte.


quelle
"Der einzige Weg, um sicher zu sein, ist, sie zu messen." -1 unwahr. Entschuldigung, aber ein anderer Beitrag hat dies als falsch erwiesen, indem er die generierte Maschinensprache verglichen und festgestellt hat, dass sie im Wesentlichen identisch ist. Ich habe kein Problem mit Ihrer Antwort im Allgemeinen, aber ist es nicht falsch, wofür die -1 ist?
Bill K
Das Untersuchen des ausgegebenen Codes ist sicherlich nützlich und kann in einem einfachen Fall wie diesem ausreichend sein. In komplexeren Fällen treten jedoch Probleme wie die Referenzlokalität hinter dem Kopf auf, und diese können nur getestet werden, indem die Ausführung zeitlich festgelegt wird.
-1

Mit nur zwei Variablen wird dem Compiler wahrscheinlich ein Register für beide zugewiesen. Diese Register sind sowieso vorhanden, daher dauert dies keine Zeit. In beiden Fällen gibt es 2 Registerschreib- und einen Registerlesebefehl.

MSalters
quelle
-2

Ich denke, dass den meisten Antworten ein wichtiger Punkt fehlt, der zu berücksichtigen ist: "Ist es klar" und offensichtlich ist bei allen Diskussionen die Tatsache; Nein ist es nicht. Ich würde vorschlagen, dass in den meisten Loop-Codes die Effizienz so gut wie kein Problem ist (es sei denn, Sie rechnen für einen Marslander). Die einzige Frage ist also, was vernünftiger und lesbarer und wartbarer aussieht - in diesem Fall würde ich die Deklaration empfehlen Die Variable vorne und außerhalb der Schleife - das macht es einfach klarer. Dann würden sich Leute wie Sie und ich nicht einmal die Mühe machen, online zu prüfen, ob es gültig ist oder nicht.

rauben
quelle
-6

Das ist nicht wahr, es gibt Overhead, aber seinen vernachlässigbaren Overhead.

Auch wenn sie wahrscheinlich an derselben Stelle auf dem Stapel landen werden, wird sie dennoch zugewiesen. Es wird Speicherplatz auf dem Stapel für dieses int zuweisen und es dann am Ende von} freigeben. Nicht im haufenfreien Sinne bewegt es sp (Stapelzeiger) um 1. Und in Ihrem Fall, wenn es nur eine lokale Variable hat, setzt es einfach fp (Rahmenzeiger) und sp gleich

Eine kurze Antwort wäre: Kümmere dich nicht darum, wie es fast genauso funktioniert.

Lesen Sie jedoch mehr darüber, wie der Stapel organisiert ist. Meine Grundschule hatte ziemlich gute Vorlesungen dazu. Wenn Sie mehr lesen möchten, lesen Sie hier http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Assembler1/lecture.html

grobartn
quelle
Wieder -1 unwahr. Lesen Sie den Beitrag, der sich mit der Versammlung befasste.
Bill K
Nein, du liegst falsch. Schauen Sie sich den Assembler-Code an, der mit diesem Code generiert wurde
grobartn