Angenommen, ich habe eine Struktur wie diese:
volatile struct { int foo; int bar; } data;
data.foo = 1;
data.bar = 2;
data.foo = 3;
data.bar = 4;
Sind die Aufgaben alle garantiert nicht nachbestellt?
Zum Beispiel wäre es dem Compiler ohne volatile erlaubt, es als zwei Anweisungen in einer anderen Reihenfolge wie folgt zu optimieren:
data.bar = 4;
data.foo = 3;
Aber muss der Compiler bei volatile so etwas nicht tun?
data.foo = 1;
data.foo = 3;
data.bar = 2;
data.bar = 4;
(Die Mitglieder als separate, nicht verwandte flüchtige Einheiten zu behandeln – und eine Neuordnung vorzunehmen, von der ich mir vorstellen kann, dass sie versuchen könnte, die Referenzlokalität für den Fall zu verbessern foo und Bar an einer Seitengrenze befinden – zum Beispiel.)
Ist die Antwort auch für aktuelle Versionen von C- und C++-Standards konsistent?
c
Sie werden nicht nachbestellt.
C17 6.5.2.3(3) sagt:
Ein Postfix-Ausdruck gefolgt von der . -Operator und ein Bezeichner bezeichnet ein Mitglied einer Struktur oder eines Vereinigungsobjekts. Der Wert ist der des benannten Members (97) und ist ein Lvalue, wenn der erste Ausdruck ein Lvalue ist. Wenn der erste Ausdruck einen qualifizierten Typ hat, hat das Ergebnis die so qualifizierte Version des Typs des bezeichneten Members.
Seit data
hat volatile
-Qualifizierter Typ, also tun data.bar
und data.foo
. Sie führen also zwei Aufgaben aus volatile int
Objekte. Und nach 6.7.3 Fußnote 136,
Aktionen auf so deklarierte Objekte [as volatile
] dürfen nicht durch eine Implementierung „herausoptimiert“ oder neu geordnet werden, es sei denn, dies ist durch die Regeln zum Auswerten von Ausdrücken zulässig.
Eine subtilere Frage ist, ob der Compiler sie beide mit einer einzigen Anweisung zuweisen könnte, z. B. wenn es sich um zusammenhängende 32-Bit-Werte handelt, könnte er einen 64-Bit-Speicher verwenden, um beide festzulegen? Ich denke nicht, und zumindest versuchen es GCC und Clang nicht.
Wenn Sie dies in mehreren Threads verwenden möchten, gibt es einen erheblichen Fallstrick.
Während der Compiler die Schreibvorgänge nicht neu anordnet volatile
Variablen (wie in der Antwort von Nate Eldredge beschrieben), gibt es einen weiteren Punkt, an dem eine Neuordnung der Schreibvorgänge auftreten kann, und das ist die CPU selbst. Dies hängt von der CPU-Architektur ab, und es folgen einige Beispiele:
Intel64
Sehen Whitepaper zur Speicherbestellung der Intel® 64-Architektur.
Während die Geschäftsanweisungen selbst nicht neu geordnet werden (2.2):
- Filialen werden nicht mit anderen Filialen nachbestellt.
Sie können für verschiedene CPUs in einer anderen Reihenfolge sichtbar sein (2.4):
Die Speicherreihenfolge von Intel 64 ermöglicht, dass Speicher von zwei Prozessoren von diesen beiden Prozessoren in unterschiedlichen Reihenfolgen gesehen werden
AMD64
AMD 64 (das übliche x64) hat ein ähnliches Verhalten in die Spezifikation:
Im Allgemeinen sind Schreibvorgänge außerhalb der Reihenfolge nicht zulässig. Außerhalb der Reihenfolge ausgeführte Schreibbefehle können ihr Ergebnis nicht in den Speicher schreiben (schreiben), bis alle vorherigen Befehle in der Programmreihenfolge abgeschlossen wurden. Der Prozessor kann jedoch das Ergebnis eines Schreibbefehls außerhalb der Reihenfolge in einem privaten Puffer (für die Software nicht sichtbar) halten, bis dieses Ergebnis im Speicher festgeschrieben werden kann.
PowerPC
Ich erinnere mich, dass ich diesbezüglich vorsichtig sein musste Xbox 360, die eine PowerPC-CPU verwendete:
Während die Xbox 360-CPU Anweisungen nicht neu ordnet, ordnet sie Schreibvorgänge neu an, die nach den Anweisungen selbst abgeschlossen werden. Dieses Umordnen von Schreibvorgängen wird speziell durch das PowerPC-Speichermodell erlaubt
Um eine CPU-Neuordnung auf tragbare Weise zu vermeiden, müssen Sie verwenden Erinnerungszäune wie C++11 std::atomic_thread_fence oder C11 atomic_thread_fence. Ohne sie kann die Reihenfolge der Schreibvorgänge, wie sie von einem anderen Thread aus gesehen wird, anders sein.
Siehe auch C++11 führte ein standardisiertes Speichermodell ein. Was bedeutet das? Und wie wird es sich auf die C++-Programmierung auswirken?
Dies ist auch in der Wikipedia vermerkt Erinnerungsbarriere Artikel:
Darüber hinaus ist nicht garantiert, dass flüchtige Lese- und Schreibvorgänge aufgrund von Caching, Cache-Kohärenzprotokoll und entspannter Speicherreihenfolge von anderen Prozessoren oder Kernen in derselben Reihenfolge gesehen werden, was bedeutet, dass flüchtige Variablen allein möglicherweise nicht einmal als Inter-Thread-Flags oder Mutexe funktionieren .
Ich weiß es nicht, aber ich hoffe es, sonst könnten die Warteschlangenstrukturen, die ich für Interrupt-Kommunikation verwende, in Schwierigkeiten geraten 🙂
– Martin Jakob
14. Dezember 2020 um 20:26 Uhr
Nicht neu geordnetes vollständiges Zitat hier für C++ (C kann anders sein) – de.cppreference.com/w/cpp/language/cv “ein Objekt, dessen Typ flüchtig qualifiziert ist, oder ein Unterobjekt eines flüchtigen Objekts” … _ “Jeder Zugriff (Lese- oder Schreibvorgang, Member-Funktionsaufruf usw.), der über einen glvalue-Ausdruck des flüchtig qualifizierten Typs erfolgt, wird behandelt als sichtbarer Nebeneffekt zu Optimierungszwecken “
– Richard Critten
14. Dezember 2020 um 20:28 Uhr
@NateEldredge Ich habe nie daran gedacht, beizutreten
std::atomic
mitvolatile
. Wenn op diese Struktur für die IO-Interaktion verfügbar macht, dann verwendenvolatile
steht außer Frage. Das Tag von op deutet jedoch darauf hin, dass es sich in diesem Fall um Parallelität (Multithread-Programm) handeltstd::atomic
ist das richtige Werkzeug zu verwenden und nichtvolatile
. Vielleicht ist dies nur eine lockere Art der Tag-Benennung.– blutig
14. Dezember 2020 um 21:43 Uhr
@bloody Ich schaue in erster Linie auf C, aber da es oft subtile Unterschiede zwischen den Sprachen gibt (C ++ scheint schon lange vom Ziel entfernt zu sein, eine Obermenge zu sein), bin ich insbesondere neugierig auf Volatilität, da dies für die Portabilität von C gelten würde Code nach C++. Ja, C++ hat in der Tat viel bessere Bibliotheken, um mit solchen Dingen umzugehen.
– Ted Shaneyfelt
15. Dezember 2020 um 0:09 Uhr
Der Compiler muss nichts tun, was einen flüchtigen Zugriff ausmacht, ist implementierungsdefiniert, der Standard definiert nur eine bestimmte Ordnungsbeziehung für Zugriffe in Bezug auf beobachtbares Verhalten und die abstrakte Maschine, auf die sich die Implementierungsdokumentation beziehen kann. Die Codegenerierung wird von der Norm nicht angesprochen.
– Philippie
16. Dezember 2020 um 6:39 Uhr