Betrachten Sie diesen Code:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
Welcher Array-Eintrag wird aktualisiert? 0 oder 2?
Gibt es einen Teil in der Spezifikation von C, der den Vorrang des Betriebs in diesem speziellen Fall angibt?
c
language-lawyer
order-of-execution
Jiminion
quelle
quelle
clang
damit dieser Code IMHO eine Warnung auslöst.Antworten:
Reihenfolge der linken und rechten Operanden
Um die Zuweisung in
arr[global_var] = update_three(2)
durchzuführen, muss die C-Implementierung die Operanden auswerten und als Nebeneffekt den gespeicherten Wert des linken Operanden aktualisieren. C 2018 6.5.16 (in dem es um Zuweisungen geht) In Absatz 3 heißt es, dass der linke und der rechte Operand keine Sequenzierung aufweisen:Dies bedeutet, dass die C-Implementierung frei ist, zuerst den l-Wert zu berechnen
arr[global_var]
(indem wir den l-Wert berechnen, meinen wir herauszufinden, worauf sich dieser Ausdruck bezieht), dann zu bewertenupdate_three(2)
und schließlich den Wert des letzteren dem ersteren zuzuweisen; oder zuerst zu bewertenupdate_three(2)
, dann den l-Wert zu berechnen und dann den ersteren dem letzteren zuzuweisen; oder um den l-update_three(2)
Wert in einer vermischten Weise zu bewerten und dann dem linken l-Wert den richtigen Wert zuzuweisen.In allen Fällen muss die Zuordnung des Wertes zum Wert zuletzt erfolgen, da 6.5.16 3 auch sagt:
Sequenzierungsverletzung
Einige denken möglicherweise über undefiniertes Verhalten nach, da
global_var
es unter Verstoß gegen 6.5 2 sowohl verwendet als auch separat aktualisiert wird.Vielen C-Praktikern ist bekannt, dass das Verhalten von Ausdrücken, wie
x + x++
es nicht durch den C-Standard definiert ist, da sie den Wert von verwendenx
und ihn im selben Ausdruck ohne Sequenzierung separat modifizieren. In diesem Fall haben wir jedoch einen Funktionsaufruf, der eine gewisse Sequenzierung bietet.global_var
wird in verwendetarr[global_var]
und im Funktionsaufruf aktualisiertupdate_three(2)
.6.5.2.2 10 sagt uns, dass es einen Sequenzpunkt gibt, bevor die Funktion aufgerufen wird:
Innerhalb der Funktion
global_var = val;
befindet sich ein vollständiger Ausdruck , ebenso wie das3
Inreturn 3;
gemäß 6.8 4:Dann gibt es einen Sequenzpunkt zwischen diesen beiden Ausdrücken, wiederum gemäß 6.8 4:
Somit kann die C-Implementierung
arr[global_var]
zuerst auswerten und dann den Funktionsaufruf ausführen. In diesem Fall befindet sich ein Sequenzpunkt zwischen ihnen, da sich vor dem Funktionsaufruf einer befindet, oder sie kannglobal_var = val;
im Funktionsaufruf auswerten und dannarr[global_var]
, in welchem Fall ein Sequenzpunkt zwischen ihnen, weil es einen nach dem vollständigen Ausdruck gibt. Das Verhalten ist also nicht spezifiziert - eines dieser beiden Dinge kann zuerst bewertet werden -, aber es ist nicht undefiniert.quelle
Das Ergebnis ist hier nicht spezifiziert .
Während die Reihenfolge der Operationen in einem Ausdruck, die bestimmen, wie Unterausdrücke gruppiert werden, genau definiert ist, ist die Reihenfolge der Auswertung nicht angegeben. In diesem Fall bedeutet dies, dass entweder
global_var
zuerst gelesen werden kann oder der Aufruf vonupdate_three
zuerst erfolgen kann, aber es gibt keine Möglichkeit zu wissen, welche.Hier gibt es kein undefiniertes Verhalten, da ein Funktionsaufruf einen Sequenzpunkt einführt, ebenso wie jede Anweisung in der Funktion, einschließlich derjenigen, die geändert wird
global_var
.Zur Verdeutlichung definiert der C-Standard undefiniertes Verhalten in Abschnitt 3.4.3 als:
und definiert nicht spezifiziertes Verhalten in Abschnitt 3.4.4 als:
Der Standard besagt, dass die Auswertungsreihenfolge der Funktionsargumente nicht angegeben ist, was in diesem Fall bedeutet, dass entweder
arr[0]
auf 3 oderarr[2]
auf 3 gesetzt wird.quelle
Ich habe es versucht und den Eintrag 0 aktualisiert.
Nach dieser Frage wird jedoch die rechte Seite eines Ausdrucks immer zuerst ausgewertet
Die Reihenfolge der Bewertung ist nicht spezifiziert und nicht geordnet. Daher denke ich, dass ein solcher Code vermieden werden sollte.
quelle
Da es wenig sinnvoll ist, Code für eine Zuweisung auszugeben, bevor Sie einen Wert zuweisen müssen, geben die meisten C-Compiler zuerst Code aus, der die Funktion aufruft, und speichern das Ergebnis irgendwo (Register, Stapel usw.), dann geben sie Code aus schreibt diesen Wert an sein endgültiges Ziel und liest daher die globale Variable, nachdem sie geändert wurde. Nennen wir dies die "natürliche Ordnung", die nicht durch irgendeinen Standard, sondern durch reine Logik definiert ist.
Während des Optimierungsprozesses versuchen die Compiler jedoch, den Zwischenschritt des vorübergehenden Speicherns des Werts irgendwo zu eliminieren und das Funktionsergebnis so direkt wie möglich in das endgültige Ziel zu schreiben. In diesem Fall müssen sie häufig zuerst den Index lesen B. in ein Register, um das Funktionsergebnis direkt in das Array verschieben zu können. Dies kann dazu führen, dass die globale Variable gelesen wird, bevor sie geändert wurde.
Dies ist also im Grunde ein undefiniertes Verhalten mit der sehr schlechten Eigenschaft, dass es sehr wahrscheinlich ist, dass das Ergebnis unterschiedlich ist, je nachdem, ob eine Optimierung durchgeführt wird und wie aggressiv diese Optimierung ist. Es ist Ihre Aufgabe als Entwickler, dieses Problem durch eine der folgenden Codierungen zu beheben:
oder Kodierung:
Als gute Faustregel gilt: Wenn globale Variablen nicht vorhanden sind
const
(oder nicht, aber Sie wissen, dass kein Code sie jemals als Nebeneffekt ändern wird), sollten Sie sie niemals direkt im Code verwenden, wie in einer Umgebung mit mehreren Threads. Auch dies kann undefiniert sein:Da der Compiler es möglicherweise zweimal liest und ein anderer Thread den Wert zwischen den beiden Lesevorgängen ändern kann. Wiederum würde eine Optimierung definitiv dazu führen, dass der Code ihn nur einmal liest, sodass Sie möglicherweise wieder andere Ergebnisse erzielen, die jetzt auch vom Timing eines anderen Threads abhängen. Somit haben Sie viel weniger Kopfschmerzen, wenn Sie globale Variablen vor der Verwendung in einer temporären Stapelvariablen speichern. Denken Sie daran, wenn der Compiler dies für sicher hält, wird er höchstwahrscheinlich auch das optimieren und stattdessen die globale Variable direkt verwenden, sodass es letztendlich keinen Unterschied in der Leistung oder der Speichernutzung machen kann.
(Nur für den Fall, dass jemand fragt, warum jemand dies tun
x + 2 * x
sollte3 * x
- bei einigen CPUs ist die Addition ultraschnell, ebenso wie die Multiplikation mit einer Potenz zwei, da der Compiler diese in Bitverschiebungen umwandelt (2 * x == x << 1
). Die Multiplikation mit beliebigen Zahlen kann jedoch sehr langsam sein Anstatt mit 3 zu multiplizieren, erhalten Sie viel schnelleren Code, indem Sie x um 1 bitverschieben und x zum Ergebnis hinzufügen - und selbst dieser Trick wird von modernen Compilern ausgeführt, wenn Sie mit 3 multiplizieren und die aggressive Optimierung aktivieren, es sei denn, es handelt sich um ein modernes Ziel CPU, bei der die Multiplikation genauso schnell ist wie die Addition, da der Trick die Berechnung verlangsamen würde.)quelle
3 * x
in zwei Lesevorgänge von x umgewandelt. Es könnte x einmal lesen und dann die x + 2 * x-Methode in dem Registerlanguage-lawyer
, in der die betreffende Sprache eine eigene "ganz besondere Bedeutung" für undefiniert hat , werden Sie nur dann Verwirrung stiften , wenn Sie sie nicht verwenden die Definition der Sprache.Globale Bearbeitung: Tut mir leid, Leute, ich war total begeistert und habe viel Unsinn geschrieben. Nur ein alter Knacker, der schimpft.
Ich wollte glauben, dass C verschont wurde, aber leider wurde es seit C11 mit C ++ gleichgesetzt. Um zu wissen, was der Compiler mit Nebenwirkungen in Ausdrücken tun wird, muss offenbar ein kleines mathematisches Rätsel gelöst werden, das eine teilweise Anordnung von Codesequenzen basierend auf einem "befindet sich vor dem Synchronisationspunkt von" beinhaltet.
Ich habe in den Tagen von K & R zufällig einige wichtige eingebettete Echtzeitsysteme entworfen und implementiert (einschließlich der Steuerung eines Elektroautos, das Menschen gegen die nächste Wand krachen lassen könnte, wenn der Motor nicht in Schach gehalten würde, ein 10-Tonnen-Industriemotor Roboter, der Menschen zu Brei zerquetschen könnte, wenn er nicht richtig befohlen wird, und eine Systemschicht, die, obwohl harmlos, ein paar Dutzend Prozessoren dazu bringt, ihren Datenbus mit weniger als 1% Systemaufwand trocken zu saugen).
Ich bin vielleicht zu senil oder dumm, um den Unterschied zwischen undefiniert und nicht spezifiziert zu erkennen, aber ich denke, ich habe immer noch eine ziemlich gute Vorstellung davon, was gleichzeitige Ausführung und Datenzugriff bedeuten. Meiner wohl informierten Meinung nach ist diese Besessenheit der C ++ - und jetzt C-Leute mit ihren Lieblingssprachen, die Synchronisationsprobleme übernehmen, ein kostspieliger Wunschtraum. Entweder wissen Sie, was gleichzeitige Ausführung ist, und Sie brauchen keines dieser Dinge, oder Sie tun es nicht, und Sie würden der ganzen Welt einen Gefallen tun, ohne zu versuchen, sich damit anzulegen.
All diese vielen atemberaubenden Abstraktionen von Speicherbarrieren sind einfach auf eine vorübergehende Reihe von Einschränkungen der Multi-CPU-Cache-Systeme zurückzuführen, die alle sicher in gängigen Betriebssystemsynchronisationsobjekten wie beispielsweise den Mutexen und Bedingungsvariablen C ++ eingekapselt werden können bietet an.
Die Kosten für diese Kapselung sind nur ein winziger Leistungsabfall im Vergleich zu einigen Fällen, die durch die Verwendung feinkörniger spezifischer CPU-Anweisungen erzielt werden könnten.
Das
volatile
Schlüsselwort (oder a#pragma dont-mess-with-that-variable
Nach allem, was ich als Systemprogrammierer getan habe, wäre es völlig ausreichend gewesen, dem Compiler zu sagen, er solle aufhören, Speicherzugriffe neu zu ordnen. Mit direkten asm-Anweisungen kann problemlos optimaler Code erstellt werden, um Treiber- und Betriebssystemcode auf niedriger Ebene mit Ad-hoc-CPU-spezifischen Anweisungen zu versehen. Ohne genaue Kenntnis der Funktionsweise der zugrunde liegenden Hardware (Cache-System oder Busschnittstelle) müssen Sie ohnehin nutzlosen, ineffizienten oder fehlerhaften Code schreiben.Eine winzige Anpassung des
volatile
Schlüsselworts und von Bob wäre jeder gewesen, außer dem Onkel des hartgesottensten Low-Level-Programmierers. Stattdessen hatte die übliche Gruppe von C ++ - Mathematikfreaks einen großen Tag damit verbracht, eine weitere unverständliche Abstraktion zu entwerfen, die ihrer typischen Tendenz entsprach, Lösungen zu entwerfen, die nach nicht existierenden Problemen suchen und die Definition einer Programmiersprache mit den Spezifikationen eines Compilers verwechseln.Nur dieses Mal war die Änderung erforderlich, um auch einen grundlegenden Aspekt von C zu entstellen, da diese "Barrieren" selbst in C-Code auf niedriger Ebene generiert werden mussten, um ordnungsgemäß zu funktionieren. Dies hat unter anderem die Definition von Ausdrücken ohne jegliche Erklärung oder Rechtfertigung verwüstet.
Zusammenfassend ist die Tatsache, dass ein Compiler aus diesem absurden Teil von C einen konsistenten Maschinencode erzeugen könnte, nur eine entfernte Folge der Art und Weise, wie C ++ - Leute mit möglichen Inkonsistenzen der Cache-Systeme der späten 2000er Jahre fertig wurden.
Ein grundlegender Aspekt von C (Ausdrucksdefinition) wurde schrecklich durcheinander gebracht, so dass die überwiegende Mehrheit der C-Programmierer - die sich zu Recht nicht um Cache-Systeme kümmern - nun gezwungen ist, sich auf Gurus zu verlassen, um das zu erklären Unterschied zwischen
a = b() + c()
unda = b + c
.Der Versuch zu erraten, was aus diesem unglücklichen Array werden wird, ist ohnehin ein Nettoverlust an Zeit und Mühe. Unabhängig davon, was der Compiler daraus machen wird, ist dieser Code pathologisch falsch. Die einzige verantwortliche Sache, die damit zu tun hat, ist, es in den Papierkorb zu schicken.
Konzeptionell können Nebenwirkungen immer aus Ausdrücken entfernt werden, mit dem trivialen Aufwand, die Änderung explizit vor oder nach der Bewertung in einer separaten Anweisung erfolgen zu lassen.
Diese Art von beschissenem Code könnte in den 80er Jahren gerechtfertigt gewesen sein, als man nicht erwarten konnte, dass ein Compiler irgendetwas optimiert. Aber jetzt, da Compiler längst schlauer geworden sind als die meisten Programmierer, bleibt nur noch ein Stück beschissener Code übrig.
Ich verstehe auch nicht, wie wichtig diese undefinierte / nicht näher bezeichnete Debatte ist. Entweder können Sie sich darauf verlassen, dass der Compiler Code mit einem konsistenten Verhalten generiert, oder Sie können nicht. Ob Sie das als undefiniert oder nicht spezifiziert bezeichnen, scheint ein strittiger Punkt zu sein.
Meiner wohl informierten Meinung nach ist C in seinem K & R-Zustand bereits gefährlich genug. Eine nützliche Entwicklung wäre das Hinzufügen von Sicherheitsmaßnahmen mit gesundem Menschenverstand. Wenn Sie beispielsweise dieses erweiterte Code-Analyse-Tool verwenden, zwingen die Spezifikationen den Compiler zur Implementierung, um zumindest Warnungen über Bonkers-Code zu generieren, anstatt stillschweigend einen Code zu generieren, der möglicherweise extrem unzuverlässig ist.
Stattdessen beschlossen die Jungs zum Beispiel, eine feste Auswertungsreihenfolge in C ++ 17 zu definieren. Jetzt wird jeder Software-Idiot aktiv dazu angeregt, absichtlich Nebenwirkungen in seinen Code zu setzen, und sich darauf verlassen, dass die neuen Compiler die Verschleierung auf deterministische Weise eifrig handhaben werden.
K & R war eines der wahren Wunder der Computerwelt. Für zwanzig Dollar haben Sie eine umfassende Spezifikation der Sprache (ich habe gesehen, wie einzelne Personen komplette Compiler geschrieben haben, nur mit diesem Buch), ein ausgezeichnetes Nachschlagewerk (das Inhaltsverzeichnis würde Sie normalerweise auf ein paar Seiten der Antwort auf Ihre Frage verweisen Frage) und ein Lehrbuch, in dem Sie lernen, die Sprache auf vernünftige Weise zu verwenden. Vervollständigen Sie mit Begründungen, Beispielen und weisen Worten der Warnung vor den zahlreichen Möglichkeiten, wie Sie die Sprache missbrauchen können, um sehr, sehr dumme Dinge zu tun.
Dieses Erbe für so wenig Gewinn zu zerstören, scheint mir eine grausame Verschwendung zu sein. Aber auch hier könnte ich den Punkt sehr wohl nicht vollständig verstehen. Vielleicht könnte mich eine freundliche Seele in die Richtung eines Beispiels für neuen C-Code weisen, der diese Nebenwirkungen erheblich ausnutzt?
quelle
0,expr,0
.