Aktualisiert, siehe unten!
Ich habe gehört und gelesen, dass C ++ 0x einem Compiler erlaubt, "Hallo" für das folgende Snippet zu drucken
#include <iostream>
int main() {
while(1)
;
std::cout << "Hello" << std::endl;
}
Es hat anscheinend etwas mit Threads und Optimierungsmöglichkeiten zu tun. Es scheint mir, dass dies viele Menschen überraschen kann.
Hat jemand eine gute Erklärung, warum dies notwendig war, um zuzulassen? Als Referenz heißt es im neuesten C ++ 0x-Entwurf unter6.5/5
Eine Schleife, die außerhalb der for-init-Anweisung im Fall einer for-Anweisung
- ruft die E / A-Funktionen der Bibliothek nicht auf und
- greift nicht auf flüchtige Objekte zu oder ändert sie nicht
- führt keine Synchronisationsoperationen (1.10) oder atomaren Operationen (Abschnitt 29) durch
kann von der Implementierung als beendet angenommen werden. [Hinweis: Dies soll Compiler-Transformationen ermöglichen, z. B. das Entfernen leerer Schleifen, auch wenn die Beendigung nicht nachgewiesen werden kann. - Endnote]
Bearbeiten:
Dieser aufschlussreiche Artikel sagt über diesen Standardtext
Leider werden die Wörter "undefiniertes Verhalten" nicht verwendet. Immer wenn der Standard sagt "der Compiler kann P annehmen", wird impliziert, dass ein Programm mit der Eigenschaft nicht-P eine undefinierte Semantik hat.
Ist das richtig und darf der Compiler "Bye" für das obige Programm drucken?
Es gibt hier einen noch aufschlussreicheren Thread , der sich mit einer analogen Änderung von C befasst, die von dem Kerl gestartet wurde, der den oben verlinkten Artikel geschrieben hat. Neben anderen nützlichen Fakten stellen sie eine Lösung vor, die auch für C ++ 0x zu gelten scheint ( Update : Dies funktioniert mit n3225 nicht mehr - siehe unten!)
endless:
goto endless;
Ein Compiler darf das anscheinend nicht optimieren, weil es keine Schleife ist, sondern ein Sprung. Ein anderer Mann fasst die vorgeschlagene Änderung in C ++ 0x und C201X zusammen
Durch das Schreiben einer Schleife behauptet der Programmierer entweder, dass die Schleife etwas mit sichtbarem Verhalten tut (E / A ausführt, auf flüchtige Objekte zugreift oder Synchronisations- oder Atomoperationen durchführt) oder dass sie schließlich beendet wird. Wenn ich gegen diese Annahme verstoße, indem ich eine Endlosschleife ohne Nebenwirkungen schreibe, lüge ich den Compiler an und das Verhalten meines Programms ist undefiniert. (Wenn ich Glück habe, warnt mich der Compiler möglicherweise davor.) Die Sprache bietet keine Möglichkeit, eine Endlosschleife ohne sichtbares Verhalten auszudrücken (nicht mehr?).
Update am 3.1.2011 mit n3225: Ausschuss hat den Text auf 1.10 / 24 verschoben und sagen
Die Implementierung kann davon ausgehen, dass ein Thread möglicherweise einen der folgenden Schritte ausführt:
- beenden,
- eine Bibliotheks-E / A-Funktion aufrufen,
- auf ein flüchtiges Objekt zugreifen oder es ändern, oder
- Führen Sie eine Synchronisationsoperation oder eine atomare Operation durch.
Der goto
Trick wird nicht mehr funktionieren!
quelle
while(1) { MyMysteriousFunction(); }
muss unabhängig kompilierbar sein, ohne die Definition dieser mysteriösen Funktion zu kennen, oder? Wie können wir also feststellen, ob Bibliotheks-E / A-Funktionen aufgerufen werden? Mit anderen Worten: Sicherlich ruft diese erste Kugel keine Funktion auf .int x = 1; for(int i = 0; i < 10; ++i) do_something(&i); x++;
infor(int i = 0; i < 10; ++i) do_something(&i); int x = 2;
? Oder möglicherweise umgekehrt,x
indem es2
vor der Schleife initialisiert wird. Es kann erkennen, dassdo_something
es sich nicht um den Wert von kümmertx
, daher ist es eine absolut sichere Optimierung, wenndo_something
sich der Wert von nichti
so ändert, dass Sie in eine Endlosschleife geraten.main() { start_daemon_thread(); while(1) { sleep(1000); } }
Programm möglicherweise sofort beendet wird, anstatt meinen Daemon in einem Hintergrundthread auszuführen?Antworten:
Ja, Hans Boehm liefert in N1528 eine Begründung dafür : Warum undefiniertes Verhalten für Endlosschleifen? Obwohl dies ein WG14-Dokument ist, gilt die Begründung auch für C ++ und das Dokument bezieht sich sowohl auf WG14 als auch auf WG21:
Der einzige wesentliche Unterschied zu C besteht darin, dass C11 eine Ausnahme für die Steuerung von Ausdrücken darstellt, bei denen es sich um konstante Ausdrücke handelt, die sich von C ++ unterscheiden und Ihr spezifisches Beispiel in C11 genau definieren.
quelle
do { x = slowFunctionWithNoSideEffects(x);} while(x != 23);
Hoisting-Code nach der Schleife nicht davon abhängt,x
erscheint dies sicher und vernünftig, lässt jedoch einen Compiler davon ausgehenx==23
erscheint es eher gefährlich als nützlich in einem solchen Code zu .Für mich ist die relevante Begründung:
Vermutlich liegt dies daran, dass es schwierig ist , die Terminierung mechanisch zu beweisen, und dass die Unfähigkeit, die Terminierung zu beweisen, Compiler behindert, die andernfalls nützliche Transformationen vornehmen könnten, z. B. das Verschieben nicht abhängiger Operationen von vor der Schleife nach nachher oder umgekehrt, wobei Post-Loop-Operationen in einem Thread ausgeführt werden Die Schleife wird in einer anderen ausgeführt und so weiter. Ohne diese Transformationen kann eine Schleife alle anderen Threads blockieren, während sie darauf warten, dass der eine Thread diese Schleife beendet. (Ich verwende "Thread" lose, um jede Form der Parallelverarbeitung zu bezeichnen, einschließlich separater VLIW-Befehlsströme.)
EDIT: Dummes Beispiel:
Hier wäre es für einen Thread schneller, dies zu tun,
complex_io_operation
während der andere alle komplexen Berechnungen in der Schleife ausführt. Ohne die von Ihnen angegebene Klausel muss der Compiler jedoch zwei Dinge beweisen, bevor er die Optimierung durchführen kann: 1) diecomplex_io_operation()
nicht von den Ergebnissen der Schleife abhängt, und 2) die Schleife wird beendet . 1) zu beweisen ist ziemlich einfach, 2) zu beweisen ist das Halteproblem. Mit der Klausel kann davon ausgegangen werden, dass die Schleife beendet wird und ein Parallelisierungsgewinn erzielt wird.Ich stelle mir auch vor, dass die Designer der Ansicht waren, dass die Fälle, in denen Endlosschleifen im Produktionscode auftreten, sehr selten sind und normalerweise ereignisgesteuerte Schleifen sind, die auf irgendeine Weise auf E / A zugreifen. Infolgedessen haben sie den seltenen Fall (Endlosschleifen) zugunsten der Optimierung des häufigeren Falls (nicht unendliche, aber schwer mechanisch zu beweisende nicht unendliche Schleifen) pessimiert.
Dies bedeutet jedoch, dass Endlosschleifen, die in Lernbeispielen verwendet werden, darunter leiden und Fallstricke im Anfängercode auslösen. Ich kann nicht sagen, dass das eine ganz gute Sache ist.
BEARBEITEN: In Bezug auf den aufschlussreichen Artikel, den Sie jetzt verlinken, würde ich sagen, dass "der Compiler X über das Programm annehmen kann" logisch äquivalent zu "wenn das Programm X nicht erfüllt, ist das Verhalten undefiniert" ist. Wir können dies wie folgt zeigen: Angenommen, es gibt ein Programm, das die Eigenschaft X nicht erfüllt. Wo würde das Verhalten dieses Programms definiert? Der Standard definiert das Verhalten nur unter der Annahme, dass die Eigenschaft X wahr ist. Obwohl der Standard das Verhalten nicht explizit als undefiniert deklariert, hat er es durch Auslassen als undefiniert deklariert.
Stellen Sie sich ein ähnliches Argument vor: "Der Compiler kann davon ausgehen, dass eine Variable x höchstens einmal zwischen Sequenzpunkten zugewiesen wird" entspricht "mehr als einmal zwischen Sequenzpunkten zuzuweisen ist undefiniert".
quelle
complex_io_operation
.Ich denke, die richtige Interpretation ist die aus Ihrer Bearbeitung: Leere Endlosschleifen sind undefiniertes Verhalten.
Ich würde nicht sagen, dass es ein besonders intuitives Verhalten ist, aber diese Interpretation ist sinnvoller als die alternative, dass der Compiler willkürlich Endlosschleifen ignorieren darf, ohne UB aufzurufen.
Wenn Endlosschleifen UB ist, es bedeutet nur , dass nicht abbrechenden Programme sind nicht aussagekräftig betrachtet: nach C ++ 0x, sie haben keine Semantik.
Das macht auch einen gewissen Sinn. Dies ist ein Sonderfall, bei dem eine Reihe von Nebenwirkungen einfach nicht mehr auftreten (z. B. wird nie etwas zurückgegeben
main
) und eine Reihe von Compiler-Optimierungen dadurch behindert werden, dass Endlosschleifen beibehalten werden müssen. Zum Beispiel ist das Verschieben von Berechnungen über die Schleife vollkommen gültig, wenn die Schleife keine Nebenwirkungen hat, da die Berechnung schließlich in jedem Fall durchgeführt wird. Wenn die Schleife jedoch niemals beendet wird, können wir den Code nicht sicher neu anordnen, da wir möglicherweise nur ändern, welche Operationen tatsächlich ausgeführt werden, bevor das Programm hängt. Es sei denn, wir behandeln ein Hängeprogramm als UB.quelle
while(1){...}
. Sie werden sogar routinemäßig verwendetwhile(1);
, um einen Watchdog-unterstützten Reset aufzurufen.Ich denke, dies entspricht der Art dieser Frage , die auf einen anderen Thread verweist . Durch die Optimierung können gelegentlich leere Schleifen entfernt werden.
quelle
X
und ein Bitmuster beendet wirdx
, kann der Compiler nachweisen, dass die Schleife nicht beendet wird, ohneX
das Bitmuster zu haltenx
. Es ist vakuum wahr. ManX
könnte also davon ausgehen, dass es ein beliebiges Bitmuster enthält, und das ist genauso schlecht wie UB in dem Sinne, dass für das FalscheX
undx
es schnell einige verursachen wird. Ich glaube, Sie müssen in Ihrem Standard präziser sein. Es ist schwierig, darüber zu sprechen, was "am Ende" einer Endlosschleife passiert, und es einer endlichen Operation gleichzusetzen.Das relevante Problem ist, dass der Compiler Code neu anordnen darf, dessen Nebenwirkungen nicht in Konflikt stehen. Die überraschende Ausführungsreihenfolge könnte auch dann auftreten, wenn der Compiler nicht terminierenden Maschinencode für die Endlosschleife erzeugt.
Ich glaube, das ist der richtige Ansatz. Die Sprachspezifikation definiert Möglichkeiten zum Erzwingen der Ausführungsreihenfolge. Wenn Sie eine Endlosschleife wünschen, die nicht neu angeordnet werden kann, schreiben Sie Folgendes:
quelle
unsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy);
, der erstefprintf
könnte ausgeführt werden, der zweite jedoch nicht (der Compiler könnte die Berechnungdummy
zwischen den beiden verschiebenfprintf
, jedoch nicht über diejenige hinaus, die seinen Wert druckt).Ich denke, das Problem könnte vielleicht am besten wie folgt angegeben werden: "Wenn ein späterer Code nicht von einem früheren Code abhängt und der frühere Code keine Nebenwirkungen auf einen anderen Teil des Systems hat, die Ausgabe des Compilers." kann den späteren Code vor, nach oder mit der Ausführung des ersteren ausführen, selbst wenn der erstere Schleifen enthält, ohne Rücksicht darauf, wann oder ob der erstere Code tatsächlich abgeschlossen werden würde . Beispielsweise könnte der Compiler Folgendes umschreiben:
wie
Im Allgemeinen nicht unangemessen, obwohl ich mir Sorgen machen könnte, dass:
würde werden
Wenn eine CPU die Berechnungen und eine andere die Fortschrittsbalkenaktualisierungen übernimmt, würde das Umschreiben die Effizienz verbessern. Leider wären die Aktualisierungen des Fortschrittsbalkens weniger nützlich, als sie sein sollten.
quelle
Für den Compiler ist es in nicht trivialen Fällen nicht entscheidbar, ob es sich überhaupt um eine Endlosschleife handelt.
In verschiedenen Fällen kann es vorkommen, dass Ihr Optimierer eine bessere Komplexitätsklasse für Ihren Code erreicht (z. B. war es O (n ^ 2) und Sie erhalten nach der Optimierung O (n) oder O (1)).
Die Aufnahme einer solchen Regel, die das Entfernen einer Endlosschleife in den C ++ - Standard nicht zulässt, würde viele Optimierungen unmöglich machen. Und die meisten Leute wollen das nicht. Ich denke, das beantwortet Ihre Frage ganz genau.
Eine andere Sache: Ich habe noch nie ein gültiges Beispiel gesehen, bei dem Sie eine Endlosschleife benötigen, die nichts bewirkt.
Das einzige Beispiel, von dem ich gehört habe, war ein hässlicher Hack, der wirklich anders gelöst werden sollte: Es handelte sich um eingebettete Systeme, bei denen der einzige Weg, einen Reset auszulösen, darin bestand, das Gerät einzufrieren, damit der Watchdog es automatisch neu startet.
Wenn Sie ein gültiges / gutes Beispiel kennen, bei dem Sie eine Endlosschleife benötigen, die nichts bewirkt, sagen Sie es mir bitte.
quelle
Ich denke, es ist erwähnenswert, dass Schleifen, die unendlich wären, außer dass sie über nichtflüchtige, nicht synchronisierte Variablen mit anderen Threads interagieren, jetzt mit einem neuen Compiler zu falschem Verhalten führen können.
Mit anderen Worten, machen Sie Ihre Globalen flüchtig - sowie Argumente, die über einen Zeiger / eine Referenz in eine solche Schleife übergeben werden.
quelle
volatile
ist weder notwendig noch ausreichend, sie herzustellen, und es beeinträchtigt die Leistung dramatisch.