Verlangsamen temporäre Variablen mein Programm?

73

Angenommen, ich habe den folgenden C-Code:

int i = 5;
int j = 10;
int result = i + j;

Wenn ich so oft eine Schleife durchführe, wäre die Verwendung dann schneller int result = 5 + 10? Ich erstelle häufig temporäre Variablen, um meinen Code besser lesbar zu machen, beispielsweise wenn die beiden Variablen aus einem Array mit einem langen Ausdruck zur Berechnung der Indizes abgerufen wurden. Ist das in Bezug auf die Leistung in C schlecht? Was ist mit anderen Sprachen?

Elliot Gorokhovsky
quelle
45
Durch die Optimierung des Compilers wird dieser Code so geändert, dass er effektiv wird: int result = 15 ;
2501
13
Der Compiler optimiert Ihren Code. Es ist produktiver, sich auf Dinge wie (einen Teil) einer Berechnung zu konzentrieren, die innerhalb einer Schleife wiederholt wird und die besser durchgeführt werden sollte, bevor die Schleife beginnt.
Wetterfahne
5
Ich denke, dass er alle temporären Variablen meint, dh: verwendet a = b + c; d = a + e; langsamer als mit a = b + c + d + e; Es kann möglicherweise mehr Speicher verbrauchen, wenn dies auf eine Weise erfolgt, die der Compiler nicht optimieren kann, aber es sollte nicht langsamer sein. Bester Fokus oder Arbeitsproduktivität, es sei denn, es handelt sich um einen kommerziellen und kritischen Leistungscode.
Aliential
1
@WeatherVane, obwohl die meisten Compiler dies zumindest bis zu einem gewissen Grad auch tun würden. Im Allgemeinen denke ich, dass es besser ist, sich auf die Wartbarkeit von Code zu konzentrieren, als auf solche Mikrooptimierungen.
FireFly
6
@ PeteBecker Ich fürchte, das ist kein produktiver Vorschlag. Es ist ziemlich einfach, so etwas zu versuchen und den falschen Eindruck zu bekommen, weil Sie zufällig einen Fall ausgewählt haben (oder nicht ausgewählt haben), der eine Ausnahme von der allgemeinen Regel darstellt. Ohne ein klares Verständnis der Funktionsweise eines Compilers sollte Sie das Testen einiger Fälle in keiner Weise davon überzeugen, dass dies für alle Fälle gilt. Solche Verallgemeinerungen können sehr riskant sein und führen oft zu Fehlern.
Jules

Antworten:

83

Ein moderner Optimierungs-Compiler sollte diese Variablen wegoptimieren , zum Beispiel wenn wir das folgende Beispiel in Godbolt mit der gccVerwendung der -std=c99 -O3Flags verwenden ( sehen Sie es live ):

#include <stdio.h>

void func()
{
  int i = 5;
  int j = 10;
  int result = i + j;

  printf( "%d\n", result ) ;
}

Dies führt zu folgender Montage:

movl    $15, %esi

Für die Berechnung von i + jist dies eine Form der konstanten Ausbreitung .

Beachten Sie, dass ich das hinzugefügt printfhabe, damit wir einen Nebeneffekt haben, sonst funcwäre weg optimiert worden für:

func:
  rep ret

Diese Optimierungen sind nach der Als-ob-Regel zulässig, nach der der Compiler nur das beobachtbare Verhalten eines Programms emulieren muss. Dies wird im Entwurf des C99-Standardabschnitts 5.1.2.3 Programmausführung behandelt, in dem es heißt:

In der abstrakten Maschine werden alle Ausdrücke gemäß der Semantik ausgewertet. Eine tatsächliche Implementierung muss keinen Teil eines Ausdrucks auswerten, wenn daraus geschlossen werden kann, dass sein Wert nicht verwendet wird und keine erforderlichen Nebenwirkungen auftreten (einschließlich solcher, die durch den Aufruf einer Funktion oder den Zugriff auf ein flüchtiges Objekt verursacht werden).

Siehe auch: Optimieren von C ++ - Code: Konstantfalten

Shafik Yaghmour
quelle
2
Wie kann ich sehen, welche Assembly mein C-Code generiert?
Syntagma
3
@ REACHUS, in gccGebrauchgcc -S -o asm_output.s app.c
David Ranieri
2
Ich denke nicht, dass dies die Frage anspricht. Das gegebene Beispiel ist einfach, aber ich denke, der Teil über "Wenn die beiden Variablen aus einem Array mit einem langen Ausdruck zur Berechnung der Indizes erhalten wurden" ist hier wichtiger, und das kann nicht weg optimiert werden, oder?
Arturo Torres Sánchez
1
@ ArturoTorresSánchez-Variablen sind nur ein menschliches Konzept. Es gibt keine wirklich Variablen. Was der Compiler sieht, sind Verweise auf Ausdrücke und dann den Assembler-Code, den sie äquivalent sind. Wenn jemand nach variablem Overhead fragt, bedeutet dies tatsächlich den Speicher-Overhead der Ergebnisassemblierungsanweisungen dieser Referenzen (z. B. Register / Speicher lesen / schreiben). "Löschen" einer Variablen bedeutet, dass der Compiler klug genug war, um zu erkennen, dass es keinen Grund gibt, diese "Referenz" erneut aus dem Speicher zu laden, da sich die Daten in einer anderen Referenz befinden, die in diesem Zusammenhang für sie erreichbar ist ... (1)
Manu343726
1
@ ArturoTorresSánchez ... (Das Array-Element in diesem Fall). Und heutzutage beherrschen Compiler globale Optimierungen und das Falten von Ausdrücken sehr gut (siehe die Antwort unten zum SSA-Formular).
Manu343726
29

Dies ist eine einfache Aufgabe für einen optimierenden Compiler. Es werden alle Variablen löschen und ersetzen resultmit 15.

Das ständige Falten in SSA-Form ist so ziemlich die grundlegendste Optimierung, die es gibt.

usr
quelle
Ja, aber da "Ergebnis" technisch nicht verwendet wird, wäre das Programm leer.
Alexyorke
Wenn dies das gesamte Programm ist, wäre es tatsächlich leer. Wenn nicht, wird 15 in alle Verwendungen "eingefügt".
usr
13

Das von Ihnen angegebene Beispiel ist für einen Compiler leicht zu optimieren. Die Verwendung lokaler Variablen zum Zwischenspeichern von Werten aus globalen Strukturen und Arrays kann die Ausführung Ihres Codes beschleunigen. Wenn Sie beispielsweise etwas aus einer komplexen Struktur in einer for-Schleife abrufen, in der der Compiler nicht optimieren kann und Sie wissen, dass sich der Wert nicht ändert, können die lokalen Variablen viel Zeit sparen.

Sie können GCC (auch andere Compiler) verwenden, um den Zwischenassemblycode zu generieren und zu sehen, was der Compiler tatsächlich tut.

Hier wird diskutiert, wie die Baugruppenlisten aktiviert werden: Verwenden Sie GCC, um eine lesbare Baugruppe zu erstellen?

Es kann lehrreich sein, den generierten Code zu untersuchen und festzustellen, was ein Compiler tatsächlich tut.

Steven Smith
quelle
Dies ist die nützlichere Antwort. Die anderen befassten sich eher mit dem ständig faltenden Teil der Dinge als mit der lokalen Kopie eines Speicherortpunkts. Das Zuweisen von Globals und Array-Elementen zu Einheimischen ist oft hilfreich, wenn der Compiler nicht nachweisen kann, dass sich Eingabearrays nicht überlappen oder dass eine unbekannte Funktion, die aufgerufen wird, keine Änderungen am Array vornehmen kann. Dies verhindert häufig, dass der Compiler denselben Speicherort mehrmals neu lädt.
Peter Cordes
10

Während alle möglichen trivialen Unterschiede zum Code das Verhalten des Compilers auf eine Weise stören können, die die Leistung geringfügig verbessert oder verschlechtert, sollte es im Prinzip keinen Leistungsunterschied machen, ob Sie temporäre Variablen wie diese verwenden, solange die Bedeutung des Programms nicht stimmt geändert. Ein guter Compiler sollte in beiden Fällen denselben oder vergleichbaren Code generieren, es sei denn, Sie erstellen absichtlich mit deaktivierter Optimierung, um Maschinencode zu erhalten, der so nah wie möglich an der Quelle liegt (z. B. zu Debugging-Zwecken).

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
3
Der Punkt "Solange die Bedeutung des Programms nicht geändert wird" ist der Schlüssel. Es gibt viele Fälle, in denen zwei Arten des Schreibens eines Programms geringfügige semantische Unterschiede aufweisen, die für einen Programmierer möglicherweise nicht von Bedeutung sind, bei denen jedoch ein Compiler viel weniger effizienten Code für einen generieren muss als für den anderen.
Supercat
1
Genau das, was Supercat gesagt hat: Der Compiler kann nicht immer beweisen, dass sich zwei Zeiger / Arrays nicht überlappen oder dass ein Funktionsaufruf den Inhalt des Speichers nicht ändern kann. Daher ist es manchmal gezwungen, mehrere Lasten desselben Speicherorts zu generieren, aber mit kann int a = arr[i]der Compiler den Wert über Funktionsaufrufe und Schreibvorgänge über andere Zeiger in einem Register behalten.
Peter Cordes
Ich stimme den Punkten von Supercat und Peter Cordes voll und ganz zu.
R .. GitHub STOP HELPING ICE
5

Sie haben das gleiche Problem wie ich, wenn ich versuche zu lernen, was ein Compiler tut - Sie erstellen ein triviales Programm, um das Problem zu demonstrieren, und untersuchen die Assembly-Ausgabe des Compilers, um festzustellen, dass der Compiler alles optimiert hat Sie haben versucht, es zu beseitigen. Sie können sogar eine ziemlich komplexe Operation in main () finden, die auf im Wesentlichen reduziert ist:

push "%i"
push 42
call printf 
ret

Ihre ursprüngliche Frage lautet nicht "Was passiert mit int i = 5; int j = 10...?" aber "Verursachen temporäre Variablen im Allgemeinen eine Laufzeitstrafe?"

Die Antwort ist wahrscheinlich nicht. Sie müssen sich jedoch die Assembly-Ausgabe für Ihren speziellen, nicht trivialen Code ansehen. Wenn Ihre CPU viele Register hat, wie z. B. ein ARM, befinden sich i und j sehr wahrscheinlich in Registern, genauso wie wenn diese Register den Rückgabewert einer Funktion direkt speichern würden. Zum Beispiel:

int i = func1();
int j = func2();
int result = i + j;

ist mit ziemlicher Sicherheit genau der gleiche Maschinencode wie:

int result = func1() + func2();

Ich schlage vor, dass Sie temporäre Variablen verwenden, wenn sie das Verstehen und Verwalten des Codes erleichtern, und wenn Sie wirklich versuchen, eine Schleife zu straffen, werden Sie trotzdem die Assembly-Ausgabe untersuchen, um herauszufinden, wie Sie so viel Leistung wie möglich verfeinern können möglich. Aber opfern Sie nicht die Lesbarkeit und Wartbarkeit für ein paar Nanosekunden, wenn dies nicht notwendig ist.

Scott
quelle
Die Lösung besteht darin, die Compilerausgabe nach Funktionen zu durchsuchen, die mit Parametern arbeiten, anstatt main()mit Konstanten zur Kompilierungszeit. Beispiel: Hier ist ein einfaches Beispiel für die Summierung eines Float-Arrays mit gcc asm-Ausgabe von godbolt: goo.gl/RxIFEF
Peter Cordes
Ich glaube, das habe ich gesagt: "Sie müssten sich die Assembly-Ausgabe für Ihren speziellen, nicht trivialen Code ansehen." ;)
Scott
Ich habe versucht zu sagen, dass man ziemlich trivialen Code in eine Funktion einfügen und den Asm dafür sehen kann. (Ich denke, dies wurde zu einem Streit über die Definition von "trivial", was nicht meine Absicht war ...). In Ihrem Beispiel wurde ein Funktionsaufruf zum Generieren von iund verwendet j. Mein Beispiel wäre, Parameter zu erstellen iund zu jfunktionieren , damit sie nur in Registern am Anfang des Codes für die Funktion sitzen.
Peter Cordes