393 Stimmen

Deklarieren von Variablen in Schleifen, gute oder schlechte Praxis?

Frage #1: Ist die Deklaration einer Variablen innerhalb einer Schleife eine gute oder schlechte Praxis?

Ich habe die anderen Threads darüber gelesen, ob es ein Leistungsproblem gibt oder nicht (die meisten sagten nein), und dass man Variablen immer so nah wie möglich an dem Ort deklarieren sollte, an dem sie verwendet werden. Was ich mich frage, ist, ob dies vermieden werden sollte, oder wenn es tatsächlich bevorzugt wird.

Beispiel:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

Frage #2: Erkennen die meisten Compiler, dass die Variable bereits deklariert wurde und überspringen diesen Teil einfach, oder wird jedes Mal ein Platz im Speicher für die Variable geschaffen?

491voto

Cyan Punkte 12362

Dies ist ausgezeichnet Praxis.

Indem Sie Variablen innerhalb von Schleifen erstellen, stellen Sie sicher, dass ihr Geltungsbereich auf die Schleife beschränkt ist. Sie kann außerhalb der Schleife weder referenziert noch aufgerufen werden.

Hier entlang:

  • Wenn der Name der Variable etwas "generisch" ist (wie "i"), besteht keine Gefahr, dass sie später im Code mit einer anderen gleichnamigen Variable vermischt wird (dies kann auch durch die Verwendung der -Wshadow Warnhinweis zum GCC)

  • Der Compiler weiß, dass der Geltungsbereich der Variablen auf den Bereich innerhalb der Schleife beschränkt ist, und gibt daher eine entsprechende Fehlermeldung aus, wenn die Variable versehentlich an anderer Stelle referenziert wird.

  • Nicht zuletzt können einige spezielle Optimierungen vom Compiler effizienter durchgeführt werden (vor allem die Registerzuweisung), da er weiß, dass die Variable nicht außerhalb der Schleife verwendet werden kann. So muss beispielsweise das Ergebnis nicht für eine spätere Wiederverwendung gespeichert werden.

Kurz gesagt, es ist richtig, dass Sie das tun.

Beachten Sie jedoch, dass die Variable nicht werthaltig sein soll zwischen jeder Schleife. In diesem Fall müssen Sie ihn möglicherweise jedes Mal neu initialisieren. Sie können auch einen größeren Block erstellen, der die Schleife umfasst und dessen einziger Zweck es ist, Variablen zu deklarieren, die ihren Wert von einer Schleife zur nächsten beibehalten müssen. Dazu gehört normalerweise der Schleifenzähler selbst.

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

Zur Frage #2: Die Variable wird einmal beim Aufruf der Funktion zugewiesen. Aus Sicht der Zuweisung ist es (fast) dasselbe wie die Deklaration der Variablen am Anfang der Funktion. Der einzige Unterschied ist der Anwendungsbereich: Die Variable kann nicht außerhalb der Schleife verwendet werden. Es ist sogar möglich, dass die Variable nicht zugewiesen wird, sondern nur einen freien Platz wiederverwendet (von einer anderen Variablen, deren Geltungsbereich beendet ist).

Mit einem eingeschränkten und präziseren Anwendungsbereich lassen sich genauere Optimierungen vornehmen. Noch wichtiger ist jedoch, dass Ihr Code dadurch sicherer wird, da Sie sich beim Lesen anderer Teile des Codes um weniger Zustände (d. h. Variablen) sorgen müssen.

Dies gilt auch außerhalb einer if(){...} blockieren. In der Regel wird anstelle von :

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

ist es sicherer, zu schreiben:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

Der Unterschied mag gering erscheinen, vor allem bei einem so kleinen Exemplar. Aber bei einer größeren Codebasis wird er helfen: Jetzt besteht kein Risiko mehr, einige result Wert von f1() a f2() blockieren. Jede result ist streng auf seinen eigenen Geltungsbereich beschränkt, was seine Rolle präziser macht. Aus der Sicht des Rezensenten ist es viel schöner, da er weniger Langfristige Zustandsvariablen um die man sich kümmern und die man verfolgen muss.

Auch der Compiler wird besser helfen: vorausgesetzt, dass in der Zukunft, nach einigen fehlerhaften Änderung des Codes, result nicht richtig initialisiert wird mit f2() . Die zweite Version weigert sich einfach, zu funktionieren, und gibt beim Kompilieren eine eindeutige Fehlermeldung aus (viel besser als zur Laufzeit). Die erste Version wird nichts entdecken, das Ergebnis von f1() wird einfach ein zweites Mal getestet und mit dem Ergebnis von f2() .

Ergänzende Informationen

Das Open-Source-Tool CppCheck (ein statisches Analysewerkzeug für C/C++-Code) bietet einige ausgezeichnete Hinweise zum optimalen Umfang von Variablen.

Als Antwort auf den Kommentar zur Zuteilung: Die obige Regel gilt in C, aber möglicherweise nicht für einige C++-Klassen.

Bei Standardtypen und -strukturen ist die Größe der Variablen zum Zeitpunkt der Kompilierung bekannt. In C gibt es keine "Konstruktion", so dass der Platz für die Variable beim Aufruf der Funktion einfach auf dem Stack zugewiesen wird (ohne Initialisierung). Aus diesem Grund entstehen bei der Deklaration der Variablen innerhalb einer Schleife "Null"-Kosten.

Bei C++-Klassen gibt es jedoch diese Konstruktor-Sache, über die ich viel weniger weiß. Ich schätze, dass die Zuweisung wahrscheinlich nicht das Problem sein wird, da der Compiler klug genug sein wird, um denselben Platz wiederzuverwenden, aber die Initialisierung wird wahrscheinlich bei jeder Schleifeniteration stattfinden.

37voto

justin Punkte 103032

Im Allgemeinen ist es eine sehr gute Praxis, sie sehr eng zu halten.

In manchen Fällen gibt es Überlegungen, wie z. B. die Leistungsfähigkeit, die es rechtfertigen, die Variable aus der Schleife herauszunehmen.

In Ihrem Beispiel erstellt und zerstört das Programm die Zeichenfolge jedes Mal. Einige Bibliotheken verwenden eine kleine String-Optimierung (SSO), so dass die dynamische Zuweisung in einigen Fällen vermieden werden kann.

Angenommen, Sie wollten diese redundanten Erstellungen/Zuweisungen vermeiden, dann würden Sie es so schreiben:

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

oder Sie können die Konstante herausziehen:

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

Erkennen die meisten Compiler, dass die Variable bereits deklariert wurde und überspringen diesen Teil einfach, oder wird jedes Mal ein Platz im Speicher für die Variable geschaffen?

Sie kann den Platz wiederverwenden, den die variabel verbraucht, und es kann Invarianten aus Ihrer Schleife ziehen. Im Fall des const char-Arrays (oben) könnte dieses Array herausgezogen werden. Der Konstruktor und der Destruktor müssen jedoch bei jeder Iteration ausgeführt werden, wenn es sich um ein Objekt handelt (wie z. B. std::string ). Im Falle der std::string Dieser "Raum" enthält einen Zeiger, der die dynamische Zuweisung für die Zeichen enthält. Also dies:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

würde in jedem Fall redundantes Kopieren und dynamisches Zuweisen und Freigeben erfordern, wenn die Variable über dem Schwellenwert für die SSO-Zeichenzahl liegt (und SSO von Ihrer Standardbibliothek implementiert wird).

Dies zu tun:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

würde immer noch bei jeder Iteration eine physische Kopie der Zeichen erfordern, aber die Form könnte zu einer einzigen dynamischen Zuweisung führen, da Sie die Zeichenkette zuweisen und die Implementierung sehen sollte, dass es nicht notwendig ist, die Größe der unterstützenden Zuweisung der Zeichenkette zu ändern. Natürlich würde man das in diesem Beispiel nicht tun (weil bereits mehrere bessere Alternativen aufgezeigt wurden), aber man könnte es in Betracht ziehen, wenn sich der Inhalt der Zeichenkette oder des Vektors ändert.

Was machen Sie also mit all diesen Optionen (und mehr)? Bleiben Sie standardmäßig sehr nah dran - bis Sie die Kosten gut verstehen und wissen, wann Sie davon abweichen sollten.

21voto

Fearnbuster Punkte 766

Ich habe nicht gepostet, um JeremyRRs Fragen zu beantworten (da sie bereits beantwortet wurden), sondern lediglich, um einen Vorschlag zu machen.

Für JeremyRR könnten Sie dies tun:

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

Ich weiß nicht, ob Sie wissen (ich wusste es nicht, als ich mit dem Programmieren anfing), dass Klammern (solange sie paarweise stehen) überall im Code platziert werden können, nicht nur nach "if", "for", "while", usw.

Mein Code wurde in Microsoft Visual C++ 2010 Express kompiliert, daher weiß ich, dass er funktioniert. Außerdem habe ich versucht, die Variable außerhalb der Klammern zu verwenden, in denen sie definiert wurde, und habe eine Fehlermeldung erhalten, daher weiß ich, dass die Variable "zerstört" wurde.

Ich weiß nicht, ob es eine schlechte Praxis ist, diese Methode zu verwenden, da viele unbeschriftete Klammern den Code schnell unleserlich machen könnten, aber vielleicht könnten einige Kommentare die Dinge klären.

16voto

Nobby Punkte 236

Bei C++ hängt es davon ab, was Sie tun. OK, es ist dummer Code, aber stellen Sie sich vor

class myTimeEatingClass
{
 public:
 //constructor
      myTimeEatingClass()
      {
          sleep(2000);
          ms_usedTime+=2;
      }
      ~myTimeEatingClass()
      {
          sleep(3000);
          ms_usedTime+=3;
      }
      const unsigned int getTime() const
      {
          return  ms_usedTime;
      }
      static unsigned int ms_usedTime;
};

myTimeEatingClass::ms_CreationTime=0; 
myFunc()
{
    for (int counter = 0; counter <= 10; counter++) {

        myTimeEatingClass timeEater();
        //do something
    }
    cout << "Creating class took " << timeEater.getTime() << "seconds at all" << endl;

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took " << timeEater.getTime() << "seconds at all" << endl;

}

Sie werden 55 Sekunden warten, bis Sie die Ausgabe von myFunc erhalten. Das liegt daran, dass jeder Schleifenkonstruktor und -destruktor zusammen 5 Sekunden für die Ausführung benötigen.

Sie benötigen 5 Sekunden, bis Sie die Ausgabe von myOtherFunc erhalten.

Natürlich ist dies ein verrücktes Beispiel.

Aber es zeigt, dass es zu einem Leistungsproblem werden könnte, wenn in jeder Schleife die gleiche Konstruktion durchgeführt wird, wenn der Konstruktor und/oder Destruktor einige Zeit benötigt.

1voto

Zoë Sparks Punkte 240

Da Ihre zweite Frage konkreter ist, werde ich sie zuerst beantworten und dann Ihre erste Frage in dem durch die zweite Frage gegebenen Kontext aufgreifen. Ich wollte eine evidenzbasierte Antwort geben, die über das hinausgeht, was hier bereits steht.

Frage #2 : Erkennen die meisten Compiler, dass die Variable bereits deklariert wurde deklariert wurde und überspringen diesen Teil einfach, oder wird jedes Mal ein jedes Mal einen Platz im Speicher an?

Sie können diese Frage selbst beantworten, indem Sie Ihren Compiler stoppen, bevor der Assembler ausgeführt wird, und sich die asm ansehen (verwenden Sie die -S Flagge, wenn Ihr Compiler eine gcc-ähnliche Schnittstelle hat, und -masm=intel wenn Sie den hier verwendeten Syntax-Stil wünschen).

In jedem Fall laden moderne Compiler (gcc 10.2, clang 11.0) für x86-64 die Variable nur bei jedem Schleifendurchlauf neu, wenn Sie die Optimierungen deaktivieren. Betrachten Sie das folgende C++-Programm - für eine intuitive Zuordnung zu asm halte ich die Dinge größtenteils im C-Stil und verwende einen Integer anstelle eines Strings, obwohl die gleichen Prinzipien im String-Fall gelten:

#include <iostream>

static constexpr std::size_t LEN = 10;

void fill_arr(int a[LEN])
{
    /* *** */
    for (std::size_t i = 0; i < LEN; ++i) {
        const int t = 8;

        a[i] = t;
    }
    /* *** */
}

int main(void)
{
    int a[LEN];

    fill_arr(a);

    for (std::size_t i = 0; i < LEN; ++i) {
        std::cout << a[i] << " ";
    }

    std::cout << "\n";

    return 0;
}

Wir können dies mit einer Version vergleichen, die den folgenden Unterschied aufweist:

    /* *** */
    const int t = 8;

    for (std::size_t i = 0; i < LEN; ++i) {
        a[i] = t;
    }
    /* *** */

Wenn die Optimierung deaktiviert ist, legt gcc 10.2 bei jedem Schleifendurchlauf der Deklaration-in-der-Schleife-Version 8 auf den Stack:

    mov QWORD PTR -8[rbp], 0
.L3:
    cmp QWORD PTR -8[rbp], 9
    ja  .L4
    mov DWORD PTR -12[rbp], 8 ;

während dies bei der Out-of-Loop-Version nur einmal geschieht:

    mov DWORD PTR -12[rbp], 8 ;
    mov QWORD PTR -8[rbp], 0
.L3:
    cmp QWORD PTR -8[rbp], 9
    ja  .L4

Wirkt sich das auf die Leistung aus? Ich habe keinen nennenswerten Unterschied in der Laufzeit zwischen ihnen mit meiner CPU (Intel i7-7700K) gesehen, bis ich die Anzahl der Iterationen in die Milliarden getrieben habe, und selbst dann war der durchschnittliche Unterschied weniger als 0,01s. Es handelt sich schließlich nur um eine einzige zusätzliche Operation in der Schleife. (Bei einer Zeichenkette ist der Unterschied bei den Operationen in der Schleife natürlich etwas größer, aber nicht so dramatisch).

Darüber hinaus ist die Frage weitgehend akademisch, denn bei einem Optimierungsgrad von -O1 oder höher gibt gcc identische asm für beide Quelldateien aus, ebenso wie clang. Zumindest in einfachen Fällen wie diesem ist es also unwahrscheinlich, dass es sich auf die Leistung auswirkt, egal wie. Natürlich sollte man bei einem realen Programm immer ein Profil erstellen, anstatt Annahmen zu treffen.

Frage 1 : Ist die Deklaration einer Variablen innerhalb einer Schleife eine gute oder schlechte Praxis?

Wie bei praktisch jeder Frage dieser Art kommt es darauf an. Wenn sich die Deklaration innerhalb einer sehr engen Schleife befindet und Sie ohne Optimierungen kompilieren, z. B. zu Debugging-Zwecken, ist es theoretisch möglich, dass das Verschieben der Deklaration außerhalb der Schleife die Leistung so weit verbessert, dass es bei Ihren Debugging-Bemühungen nützlich ist. Wenn dem so ist, könnte es sinnvoll sein, zumindest während der Fehlersuche. Und obwohl ich nicht glaube, dass es in einem optimierten Build einen Unterschied machen wird, können Sie/Ihr Paar/Ihr Team, wenn Sie einen beobachten, eine Entscheidung treffen, ob es das wert ist.

Gleichzeitig müssen Sie nicht nur berücksichtigen, wie der Compiler Ihren Code liest, sondern auch, wie er auf Menschen wirkt, Sie selbst eingeschlossen. Ich denke, Sie werden mir zustimmen, dass eine Variable, die im kleinstmöglichen Bereich deklariert ist, leichter zu überblicken ist. Wenn sie außerhalb der Schleife liegt, impliziert das, dass sie außerhalb der Schleife benötigt wird, was verwirrend ist, wenn das nicht der Fall ist. In einer großen Codebasis summieren sich kleine Verwirrungen wie diese mit der Zeit und können nach stundenlanger Arbeit ermüdend sein und zu dummen Fehlern führen. Das kann je nach Anwendungsfall viel kostspieliger sein als der Gewinn aus einer leichten Leistungsverbesserung.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X