Der C-Standard bietet Compilern viel Spielraum für Optimierungen. Die Konsequenzen dieser Optimierungen können überraschend sein, wenn Sie ein naives Programmmodell annehmen, bei dem der nicht initialisierte Speicher auf ein zufälliges Bitmuster eingestellt ist und alle Operationen in der Reihenfolge ausgeführt werden, in der sie geschrieben wurden.
Hinweis: Die folgenden Beispiele sind nur gültig, da x
ihre Adresse nie vergeben wurde und sie daher "registerartig" ist. Sie wären auch gültig, wenn die Art der x
Fallen Darstellungen hätte; Dies ist selten bei nicht signierten Typen der Fall (es erfordert mindestens ein Bit Speicherplatz und muss dokumentiert werden) und ist für unmöglich unsigned char
. Wenn x
ein vorzeichenbehafteter Typ vorhanden wäre, könnte die Implementierung das Bitmuster, das keine Zahl zwischen - (2 n-1 -1) und 2 n-1 -1 ist, als Trap-Darstellung definieren. Siehe Jens Gustedts Antwort .
Compiler versuchen, Variablen Register zuzuweisen, da Register schneller als Speicher sind. Da das Programm möglicherweise mehr Variablen verwendet, als der Prozessor Register hat, führen Compiler eine Registerzuordnung durch, was dazu führt, dass unterschiedliche Variablen dasselbe Register zu unterschiedlichen Zeiten verwenden. Betrachten Sie das Programmfragment
unsigned x, y, z;
y = 0;
z = 4;
x = - x;
y = y + z;
x = y + 1;
Wenn Zeile 3 ausgewertet wird, x
ist sie noch nicht initialisiert, daher muss Zeile 3 (aus Gründen des Compilers) eine Art Zufall sein, der aufgrund anderer Bedingungen, die der Compiler nicht klug genug war, um dies herauszufinden, nicht auftreten kann. Da z
es nicht nach Zeile 4 und x
nicht vor Zeile 5 verwendet wird, kann für beide Variablen dasselbe Register verwendet werden. Dieses kleine Programm ist also für die folgenden Operationen an Registern kompiliert:
r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
Der Endwert von x
ist der Endwert von r0
und der Endwert von y
ist der Endwert von r1
. Diese Werte sind x = -3 und y = -4 und nicht 5 und 4, wie dies bei x
ordnungsgemäßer Initialisierung der Fall wäre .
Betrachten Sie für ein ausführlicheres Beispiel das folgende Codefragment:
unsigned i, x;
for (i = 0; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Angenommen, der Compiler erkennt, dass dies condition
keine Nebenwirkungen hat. Da condition
sich nichts ändert x
, weiß der Compiler, dass der erste Durchlauf durch die Schleife möglicherweise nicht zugänglich ist, x
da er noch nicht initialisiert ist. Daher ist die erste Ausführung des Schleifenkörpers äquivalent zu x = some_value()
, es besteht keine Notwendigkeit, die Bedingung zu testen. Der Compiler kann diesen Code so kompilieren, als hätten Sie geschrieben
unsigned i, x;
i = 0;
x = some_value();
for (i = 1; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Die Art und Weise dies innerhalb des Compilers modelliert werden kann , ist zu berücksichtigen , dass jeder Wert in Abhängigkeit von x
hat , was Wert ist praktisch , solange x
nicht initialisiert ist. Da das Verhalten, wenn eine nicht initialisierte Variable nicht definiert ist, anstatt dass die Variable lediglich einen nicht angegebenen Wert hat, muss der Compiler keine spezielle mathematische Beziehung zwischen den für Sie geeigneten Werten verfolgen. Somit kann der Compiler den obigen Code folgendermaßen analysieren:
- wird während der ersten Schleifeniteration
x
nicht initialisiert, wenn die Zeit -x
ausgewertet wird.
-x
hat undefiniertes Verhalten, daher ist sein Wert was auch immer-bequem ist.
- Es gilt die Optimierungsregel , sodass dieser Code vereinfacht werden kann .
condition ? value : value
condition; value
Wenn derselbe Compiler mit dem Code in Ihrer Frage konfrontiert wird, analysiert er, dass bei der x = - x
Auswertung der Wert von -x
was auch immer zweckmäßig ist. So kann die Zuordnung weg optimiert werden.
Ich habe nicht nach einem Beispiel für einen Compiler gesucht, der sich wie oben beschrieben verhält, aber es ist die Art von Optimierungen, die gute Compiler versuchen. Ich wäre nicht überrascht, wenn ich einem begegnen würde. Hier ist ein weniger plausibles Beispiel für einen Compiler, mit dem Ihr Programm abstürzt. (Es ist möglicherweise nicht so unplausibel, wenn Sie Ihr Programm in einem erweiterten Debugging-Modus kompilieren.)
Dieser hypothetische Compiler ordnet jede Variable auf einer anderen Speicherseite zu und richtet Seitenattribute so ein, dass das Lesen aus einer nicht initialisierten Variablen einen Prozessor-Trap verursacht, der einen Debugger aufruft. Jede Zuordnung zu einer Variablen stellt zunächst sicher, dass ihre Speicherseite normal zugeordnet ist. Dieser Compiler versucht nicht, eine erweiterte Optimierung durchzuführen. Er befindet sich in einem Debugging-Modus, um Fehler wie nicht initialisierte Variablen leicht zu lokalisieren. Bei der x = - x
Auswertung verursacht die rechte Seite eine Falle und der Debugger wird gestartet.