Beim Codieren in C bin ich auf die folgende Situation gestoßen.
int function ()
{
if (!somecondition) return false;
internalStructure *str1;
internalStructure *str2;
char *dataPointer;
float xyz;
/* do something here with the above local variables */
}
Da die if
Anweisung im obigen Code von der Funktion zurückkehren kann, kann ich die Variablen an zwei Stellen deklarieren.
- Vor der
if
Aussage. - Nach der
if
Aussage.
Als Programmierer würde ich denken, die Variablendeklaration nach if
Anweisung beizubehalten.
Kostet der Deklarationsort etwas? Oder gibt es einen anderen Grund, einen Weg dem anderen vorzuziehen?
Antworten:
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 .
quelle
char *foo = "something"
, kompilieren Sie Ihren Code mit Optimierer- und Debugging-Flags (gcc -O3 -g
zum 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.+1
um zu bemerken, wie wichtig es ist, den Overhead der Gehirnleistung hier zu reduzieren ... viele Leute vergessen es.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:
int foo() { int x; return 5; // aren't we a silly little function now } int bar() { int x; int y; return 5; // still wasting our time... }
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):
foo: push ebp mov ebp, esp sub esp, 8 ; 1. this is the first line which is different between the two mov eax, 5 ; this is how we return the value add esp, 8 ; 2. this is the second line which is different between the two ret bar: push ebp mov ebp, esp sub esp, 16 ; 1. this is the first line which is different between the two mov eax, 5 ; this is how we return the value add esp, 16 ; 2. this is the second line which is different between the two ret
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,
alloca
die 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:
alloca
wird auf einer darüber liegenden Ebene behandelt. Für diejenigen, die neugierig sind,alloca
neigen Implementierungen dazu, den Stapelzeiger um einen beliebigen Betrag nach unten zu bewegen. Funktionen, die verwenden,alloca
sind erforderlich, um diesen Bereich auf die eine oder andere Weise zu verfolgen und sicherzustellen, dass der Stapelzeiger vor dem Verlassen nach oben angepasst wird.alloca
.quelle
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:
Wikipedia auf Abruf , Ein anderer Platz auf dem Stapel
All dies ist natürlich implementierungs- und systemabhängig.
quelle
alloca
, aber die beiden sind miteinander verbunden .alloca
weist Speicherplatz vom Stapel zu. Erinnerung: Dies sind Implementierungsdefinitionen.push
undpop
Operationen (machen Sie tatsächlich RISC-ISAs?) Es ist nur eine Konvention - MULTICS hatte aufwärts wachsende Stapel.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.
quelle
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).
quelle
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.
quelle
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
if
Anweisung zu deklarieren .quelle
Wenn du das hast
int function () { { sometype foo; bool somecondition; /* do something with foo and compute somecondition */ if (!somecondition) return false; } internalStructure *str1; internalStructure *str2; char *dataPointer; float xyz; /* do something here with the above local variables */ }
dann ist der Stapelspeicherplatz reserviert
foo
undsomecondition
kann offensichtlich fürstr1
usw. wiederverwendet werden. Wenn Sie also nach dem deklarierenif
, 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 deklarierenstr1
usw. vor demif
; Dies erfordert jedoch, dass der Compiler / Optimierer feststellt, dass sich die Bereiche nicht "wirklich" überlappen. Durch das Setzen der Deklarationen nach demif
erleichtern Sie dieses Verhalten auch ohne Optimierung - ganz zu schweigen von der verbesserten Lesbarkeit des Codes.quelle
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, XX
Anweisung zusammenzufassen, wobeiXX
die 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
int function(int a, int b) { int x, y, z, t; if(a == 2) { return 15; } x = 1; y = 2; z = 3; t = 4; return x + y + z + t + a + b; }
Resultierende Montage
function(int, int): push rbp mov rbp, rsp mov DWORD PTR [rbp-20], edi mov DWORD PTR [rbp-24], esi cmp DWORD PTR [rbp-20], 2 jne .L2 mov eax, 15 jmp .L3 .L2: -- snip -- .L3: pop rbp ret
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 keinePUSH
Anweisungen mehr ausgeführt werden (und keinersp
relativen 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
int function(int a, int b) { if(a == 2) { return 15; } return a + b; }
Resultierende Montage
function(int, int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov DWORD PTR [rbp-8], esi cmp DWORD PTR [rbp-4], 2 jne .L2 mov eax, 15 jmp .L3 .L2: mov edx, DWORD PTR [rbp-4] mov eax, DWORD PTR [rbp-8] add eax, edx .L3: pop rbp ret
Wenn Sie sich den Code vor der vorzeitigen Rückgabe ansehen (
jmp .L3
die 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 Registernedi
undesi
gespeichert 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.
quelle
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.
quelle
Wenn es tatsächlich darauf ankam, ist die einzige Möglichkeit, die Zuordnung der Variablen zu vermeiden, wahrscheinlich:
int function_unchecked(); int function () { if (!someGlobalValue) return false; return function_unchecked(); } int function_unchecked() { internalStructure *str1; internalStructure *str2; char *dataPointer; float xyz; /* do something here with the above local variables */ }
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.
quelle
Wenn Sie Variablen nach der if-Anweisung deklarieren und sofort von der Funktion zurückgeben, schreibt der Compiler keinen Speicher im Stapel fest.
quelle