Darf der Compiler dies optimieren (gemäß C ++ 17-Standard):
int fn() {
volatile int x = 0;
return x;
}
dazu?
int fn() {
return 0;
}
Wenn ja, warum? Wenn nicht, warum nicht?
Hier einige Überlegungen zu diesem Thema: Aktuelle Compiler werden fn()
als lokale Variable kompiliert , die auf den Stapel gelegt wird, und geben sie dann zurück. Auf x86-64 erstellt gcc beispielsweise Folgendes:
mov DWORD PTR [rsp-0x4],0x0 // this is x
mov eax,DWORD PTR [rsp-0x4] // eax is the return register
ret
Soweit ich weiß, sagt der Standard nicht, dass eine lokale flüchtige Variable auf den Stapel gelegt werden sollte. Diese Version wäre also genauso gut:
mov edx,0x0 // this is x
mov eax,edx // eax is the return
ret
Hier edx
speichert x
. Aber warum jetzt hier aufhören? Da edx
und eax
beide Null sind, können wir einfach sagen:
xor eax,eax // eax is the return, and x as well
ret
Und wir haben fn()
auf die optimierte Version umgestellt. Ist diese Transformation gültig? Wenn nicht, welcher Schritt ist ungültig?
Antworten:
Nein. Der Zugriff auf
volatile
Objekte wird als beobachtbares Verhalten angesehen, genau wie E / A, ohne besondere Unterscheidung zwischen Einheimischen und Globalen.N3690, [intro.execution], ¶8
Wie genau dies beobachtbar ist, liegt außerhalb des Geltungsbereichs des Standards und fällt direkt in implementierungsspezifisches Gebiet, genau wie E / A und Zugriff auf globale
volatile
Objekte.volatile
bedeutet "du denkst, du weißt alles, was hier vor sich geht, aber es ist nicht so; vertrau mir und mach dieses Zeug, ohne zu schlau zu sein, weil ich in deinem Programm meine geheimen Sachen mit deinen Bytes mache". Dies wird tatsächlich unter [dcl.type.cv] ¶7 erklärt:quelle
Diese Schleife kann durch die Als-ob-Regel optimiert werden, da sie kein beobachtbares Verhalten aufweist:
for (unsigned i = 0; i < n; ++i) { bool looped = true; }
Dieser kann nicht:
for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }
Die zweite Schleife führt bei jeder Iteration etwas aus, was bedeutet, dass die Schleife O (n) Zeit benötigt. Ich habe keine Ahnung, was die Konstante ist, aber ich kann sie messen und habe dann die Möglichkeit, eine (mehr oder weniger) bekannte Zeit lang zu schleifen.
Ich kann das tun, weil der Standard besagt, dass der Zugriff auf flüchtige Stoffe in der richtigen Reihenfolge erfolgen muss. Wenn ein Compiler entscheiden würde, dass in diesem Fall der Standard nicht gilt, hätte ich wahrscheinlich das Recht, einen Fehlerbericht einzureichen.
Wenn der Compiler sich dafür entscheidet,
looped
ein Register zu erstellen, habe ich vermutlich kein gutes Argument dagegen. Der Wert dieses Registers muss jedoch für jede Schleifeniteration auf 1 gesetzt werden.quelle
xor ax, ax
(woax
als solche angesehen wirdvolatile x
) Version in der Frage gültig oder ungültig ist? IOW, wie lautet Ihre Antwort auf die Frage?xor ax, ax
diesen Opcode reduziert wird, kann er nicht entfernt werden, selbst wenn er nutzlos aussieht, und er kann auch nicht zusammengeführt werden. In meinem Schleifenbeispiel müsste der kompilierte Codexor ax, ax
n-mal ausgeführt werden, um die beobachtbare Verhaltensregel zu erfüllen. Hoffentlich beantwortet die Bearbeitung Ihre Frage.volatile
Objekten an und für sich eine Art Nebeneffekt sind. Eine Implementierung könnte ihre Semantik so definieren, dass sie keine tatsächlichen CPU-Anweisungen generieren müsste. Eine Schleife, die auf ein flüchtig qualifiziertes Objekt zugreift, hat jedoch Nebenwirkungen und kann daher nicht entfernt werden.Ich widerspreche der Mehrheitsmeinung, trotz des vollständigen Verständnisses,
volatile
das beobachtbare E / A bedeutet.Wenn Sie diesen Code haben:
{ volatile int x; x = 0; }
Ich glaube, der Compiler kann es unter der Als-ob-Regel optimieren , vorausgesetzt, dass:
Die
volatile
Variable wird ansonsten nicht extern über zB Zeiger sichtbar gemacht (was hier offensichtlich kein Problem darstellt, da es im gegebenen Bereich keine solche gibt)Der Compiler bietet Ihnen keinen Mechanismus für den externen Zugriff darauf
volatile
Das Grundprinzip ist einfach, dass Sie den Unterschied aufgrund von Kriterium 2 sowieso nicht beobachten konnten.
In Ihrem Compiler ist Kriterium 2 jedoch möglicherweise nicht erfüllt ! Der Compiler versucht möglicherweise , Ihnen zusätzliche Garantien für die Beobachtung von
volatile
Variablen von "außen" zu geben, z. B. durch Analyse des Stapels. In solchen Situationen das Verhalten wirklich ist zu beobachten, so dass es nicht weg optimiert werden kann.Die Frage ist nun, unterscheidet sich der folgende Code von dem oben genannten?
{ volatile int x = 0; }
Ich glaube, ich habe in Visual C ++ ein anderes Verhalten in Bezug auf die Optimierung beobachtet, bin mir aber nicht ganz sicher, aus welchen Gründen. Kann es sein, dass die Initialisierung nicht als "Zugriff" zählt? Ich bin mir nicht sicher. Dies kann eine separate Frage wert sein, wenn Sie interessiert sind, aber ansonsten glaube ich, dass die Antwort so ist, wie ich es oben erklärt habe.
quelle
Theoretisch könnte ein Interrupt-Handler
fn()
Funktion fällt . Es kann über Instrumentierung oder angehängte Debug-Informationen auf die Symboltabelle oder die Quellzeilennummern zugreifen.x
, der mit einem vorhersagbaren Versatz vom Stapelzeiger gespeichert wird.… Damit die
fn()
Rückgabe einen Wert ungleich Null ergibt.quelle
fn()
. Die Verwendung vonvolatile
erzeugt Code-Gen, das demgcc -O0
für diese Variable entspricht: Überlaufen / Neuladen zwischen jeder C-Anweisung. (-O0
Kann immer noch mehrere Zugriffe in einer Anweisung kombinieren, ohne die Debugger-Konsistenzvolatile
x
? Was ist, wenn auf x86-64xor rax, rax
die Null enthalten ist (ich meine, das Rückgabewertregister giltx
), was natürlich von einem Debugger leicht beobachtet / geändert werden kann (dh Debug-Symbolinformationen,x
die in gespeichert sindrax
). Verstößt dies gegen den Standard?fn()
kann eingebunden werden. Mit MSVC 2017 und dem Standard-Release-Modus ist dies der Fall. Es gibt dann kein "innerhalb derfn()
Funktion". Unabhängig davon, da es sich bei der Variablen um eine automatische Speicherung handelt, gibt es keinen „vorhersehbaren Versatz“.volatile
und weil ervolatile
nicht gezwungen ist, diese Unterstützung bereitzustellen. Und so entferne ich die Ablehnung (ich habe mich geirrt), aber ich stimme nicht zu, weil ich denke, dass diese Argumentation nicht klar ist.Ich werde nur eine detaillierte Referenz für die Als-ob- Regel und das flüchtige Schlüsselwort hinzufügen . (Folgen Sie am Ende dieser Seiten den Anweisungen "Siehe auch" und "Verweise", um zu den ursprünglichen Spezifikationen zurückzukehren. Cppreference.com ist jedoch viel einfacher zu lesen / zu verstehen.)
Insbesondere möchte ich, dass Sie diesen Abschnitt lesen
Also das flüchtige Schlüsselwort speziell darum, die Compileroptimierung für glvalues zu deaktivieren . Das einzige, was das flüchtige Schlüsselwort hier beeinflussen kann, ist möglicherweise
return x
, dass der Compiler mit dem Rest der Funktion machen kann, was er will.Inwieweit der Compiler die Rückgabe optimieren kann, hängt davon ab, inwieweit der Compiler in diesem Fall den Zugriff von x optimieren darf (da er nichts neu anordnet und streng genommen den Rückgabeausdruck nicht entfernt. Es gibt den Zugriff , aber es liest und schreibt in den Stapel, der rationalisiert werden sollte.) Wenn ich es also lese, ist dies eine Grauzone, in der der Compiler optimieren darf, und kann leicht in beide Richtungen argumentiert werden.
Randnotiz: In diesen Fällen wird immer davon ausgegangen, dass der Compiler das Gegenteil von dem tut, was Sie wollten / brauchten. Sie sollten entweder die Optimierung deaktivieren (zumindest für dieses Modul) oder versuchen, ein definierteres Verhalten für das zu finden, was Sie möchten. (Dies ist auch der Grund, warum Unit-Tests so wichtig sind.) Wenn Sie glauben, dass es sich um einen Fehler handelt, sollten Sie ihn mit den Entwicklern von C ++ besprechen.
Das alles ist immer noch sehr schwer zu lesen. Versuchen Sie also, das einzubeziehen, was ich für relevant halte, damit Sie es selbst lesen können.
als ob Regel
Wenn Sie die technischen Daten lesen möchten, müssen Sie diese meiner Meinung nach lesen
quelle
_AddressOfReturnAddress
beispielsweise der Stapel analysiert. Die Leute analysieren den Stapel aus triftigen Gründen, und das ist nicht unbedingt so, weil die Funktion selbst für die Richtigkeit darauf angewiesen ist.return x;
Ich glaube, ich habe noch nie eine lokale Variable mit volatile gesehen, die kein Zeiger auf eine flüchtige war. Wie in:
int fn() { volatile int *x = (volatile int *)0xDEADBEEF; *x = 23; // request data, 23 = temperature return *x; // return temperature }
Die einzigen anderen Fälle von Volatilität, die ich kenne, verwenden eine globale, die in einen Signalhandler geschrieben ist. Keine Zeiger beteiligt. Oder Zugriff auf Symbole, die in einem Linker-Skript definiert sind und sich an bestimmten für die Hardware relevanten Adressen befinden.
Dort ist es viel einfacher zu begründen, warum die Optimierung die beobachtbaren Effekte verändern würde. Die gleiche Regel gilt jedoch für Ihre lokale flüchtige Variable. Der Compiler muss sich so verhalten, als ob der Zugriff auf x beobachtbar ist und ihn nicht wegoptimieren kann.
quelle
x
Ihr Code eine "lokale flüchtige Variable" ist. Ist es nicht.volatile
und hat nichts damit zu tun, dass es sich um einen lokalen handelt. Es könnte genauso gutstatic volatile int *const x = ...
global sein und alles, was Sie sagen, wäre immer noch genau das gleiche. Dies ist wie zusätzliches Hintergrundwissen, das notwendig ist, um die Frage zu verstehen, die vielleicht nicht jeder hat, aber es ist keine echte Antwort.