Ist die Deklaration von Variablen teuer?

75

Beim Codieren in C bin ich auf die folgende Situation gestoßen.

Da die ifAnweisung im obigen Code von der Funktion zurückkehren kann, kann ich die Variablen an zwei Stellen deklarieren.

  1. Vor der ifAussage.
  2. Nach der ifAussage.

Als Programmierer würde ich denken, die Variablendeklaration nach ifAnweisung beizubehalten.

Kostet der Deklarationsort etwas? Oder gibt es einen anderen Grund, einen Weg dem anderen vorzuziehen?

Wer bin ich
quelle
5
Soweit ich weiß, erhöht das Zuweisen von Stapelspeicher nur den Stapelzeiger. Wie viel Stapelspeicher eine Funktion benötigt, wird zur Kompilierungszeit anhand aller Variablendeklarationen bestimmt. Das Initialisieren einer Variablen geschieht zur Laufzeit, sodass die Leistung beeinträchtigt wird.
Pieter Witvoet
3
Schauen Sie sich den Assembler-Code an, der für beide Optionen generiert wurde - er sollte Ihnen eine klare Antwort geben. Stellen Sie einfach sicher, dass Sie dieselben Einstellungen für die Compileroptimierung verwenden wie für die Release-Erstellung.
Kestasx
67
Wenn Sie sich für die Leistung interessieren, sollten Sie eine Möglichkeit haben, die Leistung Ihrer Anwendung zu messen. Wenn Sie die Leistung messen können, können Sie Ihre eigene Frage beantworten: Probieren Sie es in beide Richtungen aus, und Sie werden es bald wissen. Wenn Sie keine Methode zur Leistungsmessung haben, sich aber für die Leistung interessieren, sollte Ihre Frage lauten: "Wie richte ich Tools zur Messung meiner Leistung ein?"
Eric Lippert
1
Schaffst du das in C ?
Salman A
2
@ g24l: Die Tatsache, dass die meisten Fragen auf die gleiche Weise beantwortet werden können, zeigt, warum solche Fragen nicht zu dieser Site passen. Genau dort oben mit "Was macht dieser Code?" Fragen; Sie haben den Code geschrieben, ihn ausgeführt und gesehen, was er bewirkt, und dann wissen Sie es.
Eric Lippert

Antworten:

97

In C99 und höher (oder mit der gemeinsamen konformen Erweiterung zu C89) können Sie Anweisungen und Deklarationen mischen.

Genau wie in früheren Versionen (nur umso mehr, als die Compiler intelligenter und aggressiver wurden) entscheidet der Compiler, wie Register und Stapel zugewiesen werden sollen, oder nimmt eine beliebige Anzahl anderer Optimierungen vor, die der Als-ob-Regel entsprechen.
Das bedeutet, dass in Bezug auf die Leistung kein Unterschied zu erwarten ist.

Jedenfalls war das nicht der Grund, warum dies erlaubt war:

Dies diente dazu, den Umfang einzuschränken und damit den Kontext zu verringern, den ein Mensch bei der Interpretation und Überprüfung Ihres Codes berücksichtigen muss .

Deduplikator
quelle
1
Vielen Dank für Ihre Antwort und an alle. Wenn ich Ihre Aussage richtig verstehe, kostet sie kein Leistungsproblem, bringt aber die Klarheit zum Ausdruck. Ist das richtig ?
Whoami
5
Wenn Sie den Optimierer in Aktion sehen möchten, schreiben Sie eine Funktion mit Deklarationen, die wie initialisiert werden char *foo = "something", kompilieren Sie Ihren Code mit Optimierer- und Debugging-Flags ( gcc -O3 -gzum Beispiel) und gehen Sie die Funktion im Debugger durch. Der Schrittpunkt springt herum und verzögert die Initialisierung der Variablen, bis sie benötigt wird.
Schwern
5
+1um zu bemerken, wie wichtig es ist, den Overhead der Gehirnleistung hier zu reduzieren ... viele Leute vergessen es.
44

Tun Sie, was auch immer sinnvoll ist, aber der aktuelle Codierungsstil empfiehlt, Variablendeklarationen so nah wie möglich an ihrer Verwendung zu platzieren

In der Realität sind Variablendeklarationen auf praktisch jedem Compiler nach dem ersten kostenlos. Dies liegt daran, dass praktisch alle Prozessoren ihren Stapel mit einem Stapelzeiger (und möglicherweise einem Rahmenzeiger) verwalten. Betrachten Sie beispielsweise zwei Funktionen:

Wenn ich diese auf einem modernen Compiler kompilieren würde (und ihm sagen würde, dass er nicht intelligent sein und meine nicht verwendeten lokalen Variablen optimieren soll), würde ich dies sehen (Beispiel für eine x64-Assembly .. andere sind ähnlich):

Hinweis: Beide Funktionen haben die gleiche Anzahl von Opcodes!

Dies liegt daran, dass praktisch alle Compiler den gesamten benötigten Speicherplatz im Voraus zuweisen (mit Ausnahme von ausgefallenen Dingen, allocadie separat behandelt werden). Tatsächlich ist es auf x64 obligatorisch, dass sie dies auf diese effiziente Weise tun.

(Bearbeiten: Wie Forss betonte, kann der Compiler einige der lokalen Variablen in Registern optimieren. Technisch gesehen sollte ich argumentieren, dass die erste Variable, die in den Stapel "überläuft", 2 Opcodes kostet und der Rest kostenlos ist.)

Aus den gleichen Gründen sammeln Compiler alle lokalen Variablendeklarationen und weisen ihnen direkt im Voraus Speicherplatz zu. Für C89 müssen alle Deklarationen im Voraus erfolgen, da es sich um einen 1-Pass-Compiler handelt. Damit der C89-Compiler weiß, wie viel Speicherplatz zugewiesen werden muss, muss er alle Variablen kennen, bevor der Rest des Codes ausgegeben wird. In modernen Sprachen wie C99 und C ++ werden Compiler voraussichtlich viel intelligenter sein als 1972, daher wird diese Einschränkung aus Gründen der Entwicklerfreundlichkeit gelockert.

Moderne Codierungspraktiken schlagen vor, die Variablen nahe an ihre Verwendung zu bringen

Dies hat nichts mit Compilern zu tun (die sich offensichtlich auf die eine oder andere Weise nicht darum kümmern könnten). Es wurde festgestellt, dass die meisten menschlichen Programmierer Code besser lesen, wenn die Variablen nahe an dem Ort platziert werden, an dem sie verwendet werden. Dies ist nur ein Styleguide. Sie können dem also nicht zustimmen, aber die Entwickler sind sich einig, dass dies der "richtige Weg" ist.

Nun zu ein paar Eckfällen:

  • Wenn Sie C ++ mit Konstrukteure verwenden, wird der Compiler vergeben den Platz vorne (da es schneller ist es, so zu tun, und tut nicht weh). Allerdings wird die Variable nicht sein gebaut in diesem Raum , bis die richtige Stelle in der Strömung des Codes. In einigen Fällen bedeutet dies, dass es sogar schneller sein kann, die Variablen in die Nähe ihrer Verwendung zu bringen, als sie in den Vordergrund zu stellen. Die Flusskontrolle führt uns möglicherweise um die Variablendeklaration herum. In diesem Fall muss der Konstruktor nicht einmal aufgerufen werden.
  • allocawird auf einer darüber liegenden Ebene behandelt. Für diejenigen, die neugierig sind, allocaneigen Implementierungen dazu, den Stapelzeiger um einen beliebigen Betrag nach unten zu bewegen. Funktionen, die verwenden, allocasind erforderlich, um diesen Bereich auf die eine oder andere Weise zu verfolgen und sicherzustellen, dass der Stapelzeiger vor dem Verlassen nach oben angepasst wird.
  • Es kann vorkommen, dass Sie normalerweise 16 Byte Stapelspeicher benötigen, aber unter einer Bedingung müssen Sie ein lokales Array von 50 KB zuweisen. Unabhängig davon, wo Sie Ihre Variablen in den Code einfügen, weisen praktisch alle Compiler bei jedem Aufruf der Funktion 50 KB + 16 KB Stapelspeicher zu. Dies ist selten von Bedeutung, aber in obsessiv rekursivem Code kann dies den Stapel überlaufen lassen. Sie müssen entweder den Code, der mit dem 50-KB-Array arbeitet, in eine eigene Funktion verschieben oder verwenden alloca.
  • Einige Plattformen (z. B. Windows) benötigen einen speziellen Funktionsaufruf im Prolog, wenn Sie mehr als eine Seite Stapelspeicherplatz zuweisen. Dies sollte die Analyse überhaupt nicht stark verändern (in der Implementierung ist es eine sehr schnelle Blattfunktion, die nur 1 Wort pro Seite steckt).
Cort Ammon
quelle
Bitte seien Sie explizit und sagen Sie C89, ISO C90, K & R C usw. "erfordert, dass alle Deklarationen im Voraus vorliegen, da es sich um einen 1-Pass-Compiler handelt." Sie kennen den Unterschied offensichtlich, weil Sie ausnahmsweise C99 aufrufen, aber C! = C89. Im Moment ist wohl C = C11.
Jeff Hammond
Beachten Sie auch, dass Sie {} verwenden können, um Deklarationen und Code in C89 zu mischen, vorausgesetzt, das Scoping funktioniert.
Jeff Hammond
@ Jeff: Ahh, ich wusste nicht, dass man so Zahnspangen verwenden kann. Ich schätze, das ist es, was ich als C ++ - Person bekomme, die versucht, mit C-Terminologie zu antworten!
Cort Ammon
C ++ ist eine Obermenge von C89, daher sollten Sie die meiste Zeit in Ordnung sein. C99 wird Sie jedoch erreichen, da es sich nicht um eine strikte Teilmenge von C ++ 03 handelt.
Jeff Hammond
"In Wirklichkeit sind Variablendeklarationen auf praktisch jedem Compiler nach dem ersten kostenlos." Trifft dies zu, wenn Sie erwägen, mehr Variablen zu verwenden, als der Compiler in Registern optimieren kann?
Forss
21

In C werden meiner Meinung nach alle Variablendeklarationen so angewendet, als ob sie oben in der Funktionsdeklaration stehen würden. Wenn Sie sie in einem Block deklarieren, denke ich, dass es nur eine Scoping-Sache ist (ich denke nicht, dass es in C ++ dasselbe ist). Der Compiler führt alle Optimierungen für die Variablen durch, und einige können bei höheren Optimierungen sogar effektiv im Maschinencode verschwinden. Der Compiler entscheidet dann, wie viel Speicherplatz die Variablen benötigen, und erstellt später während der Ausführung einen Speicherplatz, der als Stapel bezeichnet wird, in dem sich die Variablen befinden.

Wenn eine Funktion aufgerufen wird, werden alle Variablen, die von Ihrer Funktion verwendet werden, zusammen mit Informationen über die aufgerufene Funktion (dh die Rücksprungadresse, Parameter usw.) auf den Stapel gelegt. Es spielt keine Rolle, wo die Variable deklariert wurde, nur dass sie deklariert wurde - und sie wird unabhängig davon dem Stapel zugewiesen.

Das Deklarieren von Variablen ist an sich nicht "teuer". Wenn es einfach genug ist, nicht als Variable verwendet zu werden, wird der Compiler es wahrscheinlich als Variable entfernen.

Überprüfen Sie dies heraus:

Da Stapel

Wikipedia auf Abruf , Ein anderer Platz auf dem Stapel

All dies ist natürlich implementierungs- und systemabhängig.

Jeremy Rodi
quelle
Wie würde das mit VLAs funktionieren? Oder die Variable / ihren Stapelplatz komplett eliminieren? Oder Aliasing-Slots?
Deduplikator
@Deduplicator Anscheinend erhöht es die Größe des Stapelrahmens (gemäß dieser Präsentation). Es heißt alloca, aber die beiden sind miteinander verbunden . allocaweist Speicherplatz vom Stapel zu. Erinnerung: Dies sind Implementierungsdefinitionen.
Jeremy Rodi
Ich dachte nur, Sie mit einigen Konstrukten zu stupsen, die Ihre Erklärung gerade jetzt ausschließen würde, damit Sie sie verfeinern können.
Deduplikator
@Deduplicator Es tut mir leid? Ich glaube nicht, dass ich verstehe, was Sie sagen wollen.
Jeremy Rodi
6
@Paul Es ist nicht ungewöhnlich, den Stapel "umgekehrt" zu zeichnen, wenn Sie dies wünschen (die Oberseite des Stapels, die sich oben auf dem Papier befindet, hat offensichtliche Vorteile, wenn Sie von Hand zeichnen). Und auf jeden Fall möchten Sie HP vielleicht mitteilen, dass sie es all die Jahre falsch gemacht haben (PA-RISC lässt den Stack traditionell nach oben wachsen;) Heck, wenn Sie keine haben pushund popOperationen (machen Sie tatsächlich RISC-ISAs?) Es ist nur eine Konvention - MULTICS hatte aufwärts wachsende Stapel.
Voo
12

Ja, es kann Klarheit kosten. Wenn es einen Fall gibt, in dem die Funktion unter bestimmten Bedingungen überhaupt nichts tun darf (wie in Ihrem Fall beim Auffinden des globalen Falsches), ist es sicherlich einfacher, den Scheck oben zu platzieren, wo Sie ihn oben zeigen. etwas, das beim Debuggen und / oder Dokumentieren unerlässlich ist.

Martin James
quelle
11

Dies hängt letztendlich vom Compiler ab, aber normalerweise werden alle Einheimischen zu Beginn der Funktion zugewiesen.

Die Kosten für die Zuweisung lokaler Variablen sind jedoch sehr gering, da sie auf den Stapel gelegt werden (oder nach der Optimierung in ein Register gestellt werden).

Brainstorming
quelle
"Alle Einheimischen werden zu Beginn der Funktion zugewiesen". Meinst du, wenn ich auch einen faulen Ansatz verwende, wird er zugewiesen?
Whoami
Wahrscheinlich ja. Es liegt beim Compiler, wie es geht, aber der einfachste Weg ist, alle am Anfang zuzuweisen und alle am Ende freizugeben.
Brainstorming
2
Die Zuordnung ist jedoch nur eine einzige Add-Anweisung, daher ist sie unglaublich günstig. Überhaupt nicht wie mit Malloc.
Brainstorming
Aber wenn man sich die Antwort aller anderen Ingenieure ansieht, scheint es keine Leistung oder andere Kosten in irgendeiner Weise außer Lesbarkeit zu sein? :)
Whoami
7

Die beste Vorgehensweise besteht darin, einen faulen Ansatz anzupassen , dh sie nur dann zu deklarieren, wenn Sie sie wirklich brauchen;) (und nicht vorher). Daraus ergibt sich folgender Vorteil:

Code ist besser lesbar, wenn diese Variablen so nahe wie möglich am Verwendungsort deklariert werden.

CinCout
quelle
4
Das ist falsch (zumindest für mich). Ich finde, dass Code viel besser lesbar (und bearbeitbar - ich muss nicht durch den Code suchen, um eine Deklaration zu ändern) ist, wenn alle Variablen in einem Block am oberen Rand der Funktion deklariert sind, anstatt über den Code verteilt zu sein. Und wie andere bereits betont haben, sind Compiler intelligent genug, um die Zuordnungen zu optimieren.
Jamesqf
2
Für dich vielleicht; aber nicht für die Mehrheit der Coding-Community!
CinCout
3
Das ist falsch, Punkt. In diesem Beispiel werden Variablen verwendet, die statisch auf dem Stapel zugeordnet sind. Der Compiler generiert eine Anweisung, um Speicher auf dem Stapel für diese lokalen Variablen zu reservieren, unabhängig davon, wo sie innerhalb dieser Funktion deklariert sind.
Mark E. Haase
@binaryBaBa Ihr zweiter Punkt "Sie weisen den Variablen zu einem früheren Zeitpunkt keinen Speicher zu" bezieht sich nicht auf die Lesbarkeit des Codes, und wie andere bereits erwähnt haben, halte ich ihn nicht für richtig.
TJ
Ja, es macht für mich angesichts der verbesserten Compiler heutzutage Sinn. Entsprechend bearbeitet.
CinCout
6

Bewahren Sie die Erklärung so nahe wie möglich an dem Ort auf, an dem sie verwendet wird. Idealerweise in verschachtelten Blöcken. In diesem Fall wäre es also nicht sinnvoll, die Variablen über der ifAnweisung zu deklarieren .

Bitmaske
quelle
5

Wenn du das hast

dann ist der Stapelspeicherplatz reserviert foound someconditionkann offensichtlich für str1usw. wiederverwendet werden. Wenn Sie also nach dem deklarieren if, können Sie Stapelspeicherplatz sparen. In Abhängigkeit von den Optimierungsmöglichkeiten des Compilers, die Einsparung von Stapelspeichern kann auch stattfinden , wenn Sie die fucntion flach durch das innere Paar von Klammern zu entfernen oder wenn Sie deklarieren str1usw. vor dem if; Dies erfordert jedoch, dass der Compiler / Optimierer feststellt, dass sich die Bereiche nicht "wirklich" überlappen. Durch das Setzen der Deklarationen nach dem iferleichtern Sie dieses Verhalten auch ohne Optimierung - ganz zu schweigen von der verbesserten Lesbarkeit des Codes.

Hagen von Eitzen
quelle
5

Wenn Sie lokale Variablen in einem C-Bereich zuweisen (z. B. Funktionen), haben diese keinen Standardinitialisierungscode (z. B. C ++ - Konstruktoren). Und da sie nicht dynamisch zugewiesen werden (sie sind nur nicht initialisierte Zeiger), müssen keine zusätzlichen (und möglicherweise teuren) Funktionen aufgerufen werden (z. B. malloc), um sie vorzubereiten / zuzuweisen.

Aufgrund der Funktionsweise des Stapels bedeutet das Zuweisen einer Stapelvariablen einfach das Dekrementieren des Stapelzeigers (dh das Erhöhen der Stapelgröße, da sie bei den meisten Architekturen nach unten wächst), um Platz dafür zu schaffen. Aus Sicht der CPU bedeutet dies, dass ein einfacher SUB-Befehl ausgeführt wird: SUB rsp, 4(falls Ihre Variable 4 Byte groß ist - beispielsweise eine reguläre 32-Bit-Ganzzahl).

Wenn Sie mehrere Variablen deklarieren, ist Ihr Compiler außerdem intelligent genug, um sie tatsächlich zu einer großen SUB rsp, XXAnweisung zusammenzufassen, wobei XXdie Gesamtgröße der lokalen Variablen eines Bereichs angegeben wird. In der Theorie. In der Praxis passiert etwas anderes.

In solchen Situationen ist GCC Explorer für mich ein unschätzbares Werkzeug, um (mit enormer Leichtigkeit) herauszufinden, was "unter der Haube" des Compilers passiert.

Schauen wir uns also an, was passiert, wenn Sie tatsächlich eine Funktion wie die folgende schreiben: GCC-Explorer-Link .

C-Code

Resultierende Montage

Wie sich herausstellt, ist GCC noch schlauer. Der SUB-Befehl zum Zuweisen der lokalen Variablen wird überhaupt nicht ausgeführt. Es wird nur (intern) davon ausgegangen, dass der Speicherplatz "belegt" ist, es werden jedoch keine Anweisungen zum Aktualisieren des Stapelzeigers hinzugefügt (z SUB rsp, XX. B. ). Dies bedeutet, dass der Stapelzeiger nicht auf dem neuesten Stand gehalten wird. Da in diesem Fall jedoch keine PUSHAnweisungen mehr ausgeführt werden (und keine rsprelativen Suchvorgänge durchgeführt werden), nachdem der Stapelspeicher verwendet wurde, gibt es kein Problem.

Hier ist ein Beispiel, in dem keine zusätzlichen Variablen deklariert sind: http://goo.gl/3TV4hE

C-Code

Resultierende Montage

Wenn Sie sich den Code vor der vorzeitigen Rückgabe ansehen ( jmp .L3die zum Bereinigungs- und Rückgabecode springt), werden keine zusätzlichen Anweisungen aufgerufen, um die Stapelvariablen "vorzubereiten". Der einzige Unterschied besteht darin, dass die Funktionsparameter a und b, die in den Registern ediund esigespeichert sind, an einer höheren Adresse als im ersten Beispiel ( [rbp-4]und [rbp - 8]) auf den Stapel geladen werden . Dies liegt daran, dass für die lokalen Variablen wie im ersten Beispiel kein zusätzlicher Speicherplatz "zugewiesen" wurde. Wie Sie sehen können, ist der einzige "Overhead" für das Hinzufügen dieser lokalen Variablen eine Änderung eines Subtraktionsterms (dh nicht einmal das Hinzufügen einer zusätzlichen Subtraktionsoperation).

In Ihrem Fall fallen also praktisch keine Kosten für die einfache Deklaration von Stapelvariablen an.

Andrei Bârsan
quelle
4

Ich ziehe es vor, den "Early Out" -Zustand an der Spitze der Funktion zu belassen und zu dokumentieren, warum wir dies tun. Wenn wir es nach einer Reihe von Variablendeklarationen setzen, könnte jemand, der mit dem Code nicht vertraut ist, ihn leicht übersehen, es sei denn, er weiß, dass er danach suchen muss.

Die Dokumentation des "Early Out" -Zustands allein reicht nicht immer aus, es ist besser, dies auch im Code klar zu machen. Wenn Sie die Early-Out-Bedingung oben einfügen, ist es auch einfacher, das Dokument mit dem Code synchron zu halten, wenn wir später entscheiden, die Early-Out-Bedingung zu entfernen oder weitere solche Bedingungen hinzuzufügen.

Maskierter Mann
quelle
4

Wenn es tatsächlich darauf ankam, ist die einzige Möglichkeit, die Zuordnung der Variablen zu vermeiden, wahrscheinlich:

Aber in der Praxis werden Sie wahrscheinlich keinen Leistungsvorteil finden. Wenn überhaupt ein winziger Aufwand.

Wenn Sie C ++ codieren und einige dieser lokalen Variablen nicht triviale Konstruktoren haben, müssen Sie diese wahrscheinlich nach der Überprüfung platzieren. Aber selbst dann denke ich nicht, dass es helfen würde, die Funktion aufzuteilen.

Persixty
quelle
1

Wenn Sie Variablen nach der if-Anweisung deklarieren und sofort von der Funktion zurückgeben, schreibt der Compiler keinen Speicher im Stapel fest.

Thomas Papamihos
quelle
@ ThomasPapamilhos: Nicht so. Die Antwort ist eigentlich undefiniert, aber meiner Erfahrung nach werden die meisten Compiler den gesamten Speicherplatz zuweisen, um die Funktion bei der Eingabe abzuschließen, um wiederholte Bewegungen des Stapelzeigers zu vermeiden, die fast nichts gewinnen.
Persixty