-1

Während einer Diskussion hatte ich mit ein paar Kollegen neulich ein Stück Code in C++ zusammen, um eine Speicherzugriffsverletzung zu veranschaulichen.Wird die Inkonsistenz der Compiler-Optimierung vollständig durch undefiniertes Verhalten erklärt?

Ich bin gerade dabei, langsam nach C++ zurückzukehren, nachdem ich fast ausschließlich Sprachen mit Garbage Collection benutzt habe, und ich denke, mein Touchverlust zeigt, da ich durch das Verhalten meiner Short ziemlich verwirrt bin Programm ausgestellt.

Der Code in Frage wie zum Beispiel:

#include <iostream> 

using std::cout; 
using std::endl; 

struct A 
{ 
    int value; 
}; 

void f() 
{ 
    A* pa; // Uninitialized pointer 
    cout<< pa << endl; 
    pa->value = 42; // Writing via an uninitialized pointer 
} 

int main(int argc, char** argv) 
{ 
    f(); 

    cout<< "Returned to main()" << endl; 
    return 0; 
} 

I mit GCC 4.9.2 auf Ubuntu 15.04 mit -O2 Compiler-Flag Set zusammengestellt. Meine Erwartungen beim Ausführen waren, dass es abstürzen würde, wenn die Zeile, die durch meinen Kommentar als "Schreiben über einen nicht initialisierten Zeiger" bezeichnet wurde, ausgeführt wurde.

Entgegen meiner Erwartungen jedoch lief das Programm erfolgreich zu Ende, produziert die folgende Ausgabe:

0 
Returned to main() 

ich den Code mit einem -O0 Flag neu kompiliert (um alle Optimierungen zu deaktivieren) und lief das Programm erneut . Dieses Mal war das Verhalten wie ich erwartet hatte:

0 
Segmentation fault 

(Na ja, fast: Ich kann nicht ein Zeiger werden initialisiert auf 0 erwartet hatte) Basierend auf dieser Beobachtung, nehme ich an, dass beim Kompilieren mit -O2 Satz, der fatale Befehl wurde weg optimiert. Dies ist sinnvoll, da kein weiterer Code auf den pa->value zugreift, nachdem er von der fehlerhaften Zeile gesetzt wurde, so dass der Compiler vermutlich festgestellt hat, dass seine Entfernung das beobachtbare Verhalten des Programms nicht verändern würde.

Ich reproduzierte dies mehrmals und jedes Mal, wenn das Programm abstürzen würde, wenn ohne Optimierung kompiliert und wunderbar funktionieren, wenn mit -O2 kompiliert.

wurde Meine Hypothese weiter bestätigt, wenn ich eine Zeile hinzugefügt, die die pa->value ausgibt, bis zum Ende des f() ‚s Körpers:

cout<< pa->value << endl; 

Genau wie bei dieser Linie an Ort und Stelle zu erwarten, stürzt das Programm, konsequent , unabhängig von der Optimierungsstufe, mit der es kompiliert wurde.

Das alles macht Sinn, wenn meine Annahmen soweit stimmen. Allerdings , wo mein Verständnis etwas bricht, ist für den Fall, wo ich den Code aus dem Körper von f() direkt an main() bewegen, etwa so:

int main(int argc, char** argv) 
{ 
    A* pa; 
    cout<< pa << endl; 
    pa->value = 42; 
    cout<< pa->value << endl; 

    return 0; 
} 

Mit Optimierungen deaktiviert, dieses Programm stürzt ab, wie erwartet. Mit -O2 jedoch läuft das Programm erfolgreich zu Ende und erzeugt die folgende Ausgabe:

0 
42 

Und das macht keinen Sinn für mich.

This answer erwähnt "Dereferenzierung eines Zeigers, der noch nicht definitiv initialisiert wurde", was genau ich mache, als eine der Quellen von undefiniertem Verhalten in C++.

So ist dieser Unterschied in der Art und Weise Optimierung wirkt sich auf den Code in main(), verglichen mit dem Code in f(), vollständig durch die Tatsache erklärt, dass mein Programm enthält UB und damit Compiler ist technisch frei zu „ausrasten“, oder Gibt es einen grundlegenden Unterschied, von dem ich nicht weiß, ob der Code in main() optimiert ist, verglichen mit Code in anderen Routinen?

+1

Die beiden Alternativen in Ihrem letzten Absatz schließen sich nicht gegenseitig aus. –

+1

Wenn UB beteiligt ist, können Sie nicht erwarten, dass der C++ - Standard das resultierende Programm und sein Ergebnis beschreibt. ("... stellt keine Anforderungen") – milleniumbug

+1

@BenjaminLindley Nun, ich sagte "oder", nicht "xor". :) – TerraPass

Antwort

1

Schreiben von unbekannten Zeigern war schon immer etwas, das unbekannte Folgen haben könnte. Was ärgerlicher ist, ist eine derzeit modische Philosophie, die vorschlägt, dass Compiler davon ausgehen sollten, dass Programme niemals Eingaben erhalten, die UB verursachen, und daher jeden Code optimieren sollten, der auf solche Eingaben prüfen würde, wenn solche Tests UB nicht verhindern würden.

So zum Beispiel gegeben:

uint32_t hey(uint16_t x, uint16_t y) 
{ 
    if (x < 60000) 
    launch_missiles(); 
    else 
    return x*y; 
} 
void wow(uint16_t x) 
{ 
    return hey(x,40000); 
} 

ein 32-Bit-Compiler legitim wow mit einem bedingungslosen Aufruf launch_missiles ohne Rücksicht auf den Wert von x, da x "kann unmöglich" ersetzen könnten größer sein als 53687 (jeder Wert darüber hinaus würde die Berechnung von x * y zum Überlaufen bringen. Obwohl die Autoren von C89 angaben, dass die Mehrheit der Compiler dieser Ära das korrekte Ergebnis in einer Situation wie der obigen berechnen würde, seit dem Standard stellt keine Anforderungen an Compiler, hypermoderne Philosophie hält es für "effizienter" "Damit Compiler Programme annehmen, werden sie niemals Eingaben erhalten, die auf solche Dinge angewiesen wären.

+0

Das ist interessant. Also, heißt es, dass in der Praxis die gröbsten Fälle von undefiniertem Verhalten tatsächlich aufgrund aggressiver Compiler-Optimierungen auftreten?(Mit "ungeheuerlich" meine ich nicht einfach einen Absturz, sondern das Programm führt Dinge aus, die der Programmierer nie beabsichtigt hat.) – TerraPass

+0

(Ich verstehe das * theoretisch *, selbst wenn die Optimierung vollständig deaktiviert ist, können keine Annahmen über das Verhalten gemacht werden des Programms, das UB enthält. Aber der Code, den ich gepostet habe, verhielt sich in vorhersehbarer Weise (verursachte Segmentierungsfehler), wenn Optimierungen deaktiviert waren, deshalb bin ich neugierig. – TerraPass

+0

@TerraPass: Eine einfache Interpretation der Bedeutung Ihres Programms wäre "Schreiben Sie einen Wert irgendwo in Erinnerung - es ist mir egal wo". Wie ich in meinem ersten Satz gesagt habe, weil es fast keine Grenzen gibt, welche Aktionen ausgelöst werden könnten, wenn man in Bereiche des Gedächtnisses schreibt, ist das Schreiben in einen unbekannten Bereich des Gedächtnisses ein allgemein anerkanntes Rezept für ein Desaster. Auf vielen Computern löst das Schreiben in bestimmte Speicherbereiche einen Segmentierungsfehler aus. Da Sie nicht gesagt haben, wo Sie den geschriebenen Wert haben wollen, sollten Sie nicht überrascht sein, wenn der Compiler willkürlich einen Bereich auswählt, der einen solchen Fehler ausgelöst hat. – supercat

2

Ihr Programm hat ein undefiniertes Verhalten. Dies bedeutet, dass alles passieren kann. Das Programm wird vom C++ Standard überhaupt nicht abgedeckt. Sie sollten nicht mit irgendwelchen Erwartungen gehen.

Es wird oft gesagt, dass undefiniertes Verhalten "Raketen abfeuern" oder "Dämonen aus der Nase fliegen lassen" kann, um diesen Punkt zu verstärken. Letzteres ist weit hergeholt, aber das erste ist machbar, stellen Sie sich vor, Ihr Code ist auf einem nuklearen Startplatz und der wilde Zeiger passiert, ein Stück Speicher zu schreiben, der globalen thermoclear Krieg beginnt.

+0

Ah, ist das, wo die Phrase "nasale Dämonen", routinemäßig von Kommentatoren auf SO tatsächlich verwendet, kommt? Ich dachte immer, dass die Leute es sarkastisch anwendeten, was bedeutet, dass "du nicht genug Code gegeben hast, um deine Frage zu beantworten". Aber jetzt sehe ich, dass es sich tatsächlich auf UB bezieht. – TerraPass

+0

Es ist ein jahrzehntelanger Begriff, und es bedeutet die Auswirkungen von UB –

+0

Könnten Sie bitte einen Blick auf [mein Kommentar] (http://stackoverflow.com/questions/36611401/is-this-compiler-optimization-inconsency-) vollständig erklärt-durch-undefiniert-Beh # comment60841836_36614702) unter @ supercat's Antwort? Ich verstehe, dass der Standard keine Anforderungen an Programme stellt, die UB aufrufen. Allerdings bin ich gespannt, ob * in der Praxis * eine Verbindung zwischen Optimierung und der Natur von UB besteht, die solche Programme aufweisen. – TerraPass