Frage Führt das C ++ - flüchtige Schlüsselwort einen Speicherzaun ein?


ich verstehe das volatile Informiert den Compiler, dass der Wert geändert werden kann. Um diese Funktionalität zu erreichen, muss der Compiler jedoch einen Memory Fence einführen, damit er funktioniert.

Aus meiner Sicht kann die Reihenfolge der Operationen auf flüchtigen Objekten nicht neu geordnet werden und muss erhalten werden. Dies scheint zu implizieren, dass einige Erinnerungszäune notwendig sind und dass es keinen wirklichen Weg gibt. Stimmt das richtig?


Es gibt eine interessante Diskussion bei diese verwandte Frage

Jonathan Wakely schreibt:

... Zugriffe auf bestimmte volatile Variablen können nicht neu geordnet werden   Compiler, solange sie in separaten vollständigen Ausdrücken auftreten ... richtig   dieser Flüchtige ist nutzlos für die Fadensicherheit, aber nicht für die Gründe, die er hat   gibt. Das liegt nicht daran, dass der Compiler die Zugriffe neu ordnen könnte   flüchtige Objekte, aber weil die CPU sie neu anordnen könnte. Atomar   Operationen und Speicherbarrieren verhindern den Compiler und die CPU   Neuordnung

Zu welchem David Schwartz Antworten in den Kommentaren:

... Es gibt keinen Unterschied, aus der Sicht des C ++ Standards,   zwischen dem Compiler etwas tun und dem Compiler emittieren   Anweisungen, die die Hardware dazu veranlassen, etwas zu tun. Wenn die CPU es könnte   Zugriffe auf flüchtige Stoffe neu ordnen, dann erfordert der Standard dies nicht   ihre Reihenfolge wird beibehalten. ...

... Der C ++ - Standard macht keinen Unterschied darüber, was der   Neuordnung. Und Sie können nicht behaupten, dass die CPU sie mit nein neu anordnen kann   beobachtbarer Effekt, das ist in Ordnung - der C ++ Standard definiert ihre   Ordnung als beobachtbar. Ein Compiler entspricht dem C ++ - Standard auf   eine Plattform, wenn es Code erzeugt, der die Plattform macht, was die   Standard erfordert. Wenn der Standard Zugriffe auf flüchtige Stoffe erfordert, nicht   nachbestellt werden, dann ist eine Plattform, die sie neu anordnet, nicht konform. ...

Mein Punkt ist, dass wenn der C ++ - Standard den Compiler verbietet   Neuordnung der Zugänge zu verschiedenen flüchtigen Stoffen, nach der Theorie, dass die   Die Reihenfolge solcher Zugriffe ist Teil des beobachtbaren Verhaltens des Programms.   Dann muss der Compiler auch Code ausgeben, der die CPU verbietet   von daher. Der Standard unterscheidet nicht zwischen dem, was   Compiler macht und was der Compiler generiere, macht die CPU.

Was ergibt zwei Fragen: Ist einer von ihnen "richtig"? Was machen tatsächliche Implementierungen wirklich?


75
2017-10-10 19:51


Ursprung


Antworten:


Anstatt zu erklären, was volatile Erlauben Sie mir zu erklären, wann Sie verwenden sollten volatile.

  • Wenn in einem Signalhandler. Weil Schreiben an a volatile Variable ist das einzige, was der Standard von einem Signal-Handler aus ermöglicht. Seit C ++ 11 können Sie verwenden std::atomic zu diesem Zweck, aber nur, wenn das Atom frei ist.
  • Beim Umgang mit setjmp  laut Intel.
  • Wenn Sie direkt mit Hardware arbeiten und sicherstellen möchten, dass der Compiler Ihre Lese- oder Schreibvorgänge nicht optimiert.

Beispielsweise:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

Ohne das volatile Spezifizierer, der Compiler darf die Schleife vollständig optimieren. Das volatile Der Spezifizierer teilt dem Compiler mit, dass er nicht davon ausgehen darf, dass 2 nachfolgende Lesevorgänge den gleichen Wert zurückgeben.

Beachten Sie, dass volatile hat nichts mit Threads zu tun. Das obige Beispiel funktioniert nicht, wenn ein anderer Thread geschrieben wurde *foo weil es keine Erwerbstätigkeit gibt.

In allen anderen Fällen wird die Verwendung von volatile sollte als nicht portabel betrachtet werden und keine Codeüberprüfung mehr durchführen, außer wenn es sich um C ++ - 11-Compiler und -Compiler-Erweiterungen (wie MSVCs) handelt /volatile:ms Schalter, der standardmäßig unter X86 / I64 aktiviert ist).


45
2017-10-10 21:44



Führt das C ++ - flüchtige Schlüsselwort einen Speicherzaun ein?

Ein C ++ - Compiler, der der Spezifikation entspricht, muss keinen Memory Fence einführen. Ihr bestimmter Compiler könnte; richten Sie Ihre Frage an die Autoren Ihres Compilers.

Die Funktion "flüchtig" in C ++ hat nichts mit Threading zu tun. Denken Sie daran, der Zweck von "volatile" besteht darin, Compiler-Optimierungen zu deaktivieren, so dass das Lesen von einem Register, das sich aufgrund exogener Bedingungen ändert, nicht weg optimiert wird. Ist eine Speicheradresse, auf die von einem anderen Thread auf einer anderen CPU geschrieben wird, ein Register, das sich aufgrund exogener Bedingungen ändert? Nein. Wieder, wenn einige Compiler-Autoren haben gewählt um Speicheradressen zu behandeln, auf die von verschiedenen Threads auf verschiedenen CPUs geschrieben wird, als wären sie Register, die sich aufgrund exogener Bedingungen ändern, das ist ihr Geschäft; Sie sind dazu nicht verpflichtet. Sie sind auch nicht erforderlich - selbst wenn sie einen Memory Fence einführen -, um zum Beispiel dafür zu sorgen jeden Thread sieht a konsistent Bestellung von flüchtigen Lese- und Schreibvorgängen.

In der Tat ist volatile für das Threading in C / C ++ ziemlich nutzlos. Best Practice ist es zu vermeiden.

Außerdem: Speicherzäune sind ein Implementierungsdetail bestimmter Prozessorarchitekturen. In C #, wo explizit flüchtig ist Die Spezifikation, die für Multithreading entwickelt wurde, besagt nicht, dass Halbzäune eingeführt werden, da das Programm möglicherweise auf einer Architektur ausgeführt wird, die überhaupt keine Zäune hat. Vielmehr gibt die Spezifikation bestimmte (extrem schwache) Garantien darüber ab, welche Optimierungen vom Compiler, von der Laufzeit und von der CPU vermieden werden, um bestimmte (extrem schwache) Einschränkungen für die Anordnung einiger Nebenwirkungen zu treffen. In der Praxis werden diese Optimierungen durch die Verwendung von Halbzäunen eliminiert, aber dies ist ein Implementierungsdetail, das sich in der Zukunft ändern wird.

Die Tatsache, dass Ihnen die Bedeutung von volatile Sprachen in jeder Sprache wichtig ist, da sie Multithreading betreffen, deutet darauf hin, dass Sie daran denken, Speicher über Threads gemeinsam zu nutzen. Denk einfach nicht daran. Es macht Ihr Programm viel schwieriger zu verstehen und enthält viel eher subtile, unmöglich zu reproduzierende Fehler.


20
2017-10-11 13:39



Was David übersieht, ist die Tatsache, dass der C ++ - Standard das Verhalten mehrerer Threads spezifiziert, die nur in bestimmten Situationen interagieren und alles andere zu undefiniertem Verhalten führt. Eine Racebedingung mit mindestens einem Schreibvorgang ist nicht definiert, wenn Sie keine atomaren Variablen verwenden.

Folglich ist der Compiler vollkommen in seinem Recht, auf Synchronisationsanweisungen zu verzichten, da Ihre CPU nur den Unterschied in einem Programm bemerken wird, das aufgrund fehlender Synchronisation undefiniertes Verhalten aufweist.


12
2017-10-10 22:36



Zunächst einmal garantieren die C ++ - Standards nicht die Speicherbarrieren, die für die richtige Anordnung der Lese- / Schreiboperationen, die nicht atomar sind, benötigt werden. flüchtig Variablen werden für die Verwendung mit MMIO, Signalverarbeitung usw. empfohlen. Bei den meisten Implementierungen flüchtig ist nicht nützlich für Multithreading und es wird nicht generell empfohlen.

Hinsichtlich der Implementierung von flüchtigen Zugriffen ist dies die Wahl des Compilers.

Dies Artikelbeschreibend gcc Das Verhalten zeigt, dass Sie ein flüchtiges Objekt nicht als Speicherbarriere verwenden können, um eine Folge von Schreibvorgängen im flüchtigen Speicher anzuordnen.

Bezüglich icc Verhalten Ich habe das gefunden Quelle zu sagen, dass volatile nicht die Bestellung von Speicherzugriffen garantiert.

Microsoft VS2013 Compiler hat ein anderes Verhalten. Dies Dokumentation erläutert, wie volatile Erzwinge Semantik erzwingen / ermöglichen und flüchtige Objekte in Locks / Releases in Multithread-Anwendungen verwenden kann.

Ein weiterer Aspekt, der in Betracht gezogen werden muss, ist, dass derselbe Compiler a unterschiedliches Verhalten wrt. abhängig von der Zielhardwarearchitektur. Dies Post In Bezug auf den MSVS 2013-Compiler werden die Besonderheiten der Kompilierung mit Volatile für ARM-Plattformen eindeutig angegeben.

Also meine Antwort an:

Führt das C ++ - flüchtige Schlüsselwort einen Speicherzaun ein?

wäre: Nicht garantiert, wahrscheinlich nicht, aber einige Compiler könnten es tun. Sie sollten sich nicht darauf verlassen, dass dies der Fall ist.


12
2017-10-10 19:55



Der Compiler fügt, soweit ich weiß, nur einen Speicherzaun in die Itanium-Architektur ein.

Das volatile Schlüsselwort wird wirklich am besten für asynchrone Änderungen verwendet, z. B. Signalhandler und speicherabgebildete Register; Es ist normalerweise das falsche Werkzeug für Multithread-Programmierung.


7
2017-10-10 19:55



Es hängt davon ab, welcher Compiler "der Compiler" ist. Visual C ++ tut dies seit 2005. Aber der Standard erfordert es nicht, also einige andere Compiler nicht.


6
2017-10-10 20:03



Dies ist weitgehend aus dem Speicher und basierend auf Pre-C ++ 11, ohne Threads. Aber Nachdem ich an Diskussionen über Threading im Committee teilgenommen habe, kann ich das sagen Es gab nie eine Absicht des Komitees das volatile könnte für verwendet werden Synchronisation zwischen Threads. Microsoft hat es vorgeschlagen, aber den Vorschlag hat nicht getragen.

Die Schlüsselspezifikation von volatile ist, dass der Zugriff auf eine volatile repräsentiert ein "beobachtbares Verhalten", genau wie IO. Genauso kann der Compiler nicht Umordnen oder entfernen Sie bestimmte IO, kann es nicht neu ordnen oder entfernen Zugriffe auf eine flüchtiges Objekt (oder genauer gesagt, greift auf einen lvalue Ausdruck mit flüchtiger qualifizierter Typ). Die ursprüngliche Absicht von volatile war in der Tat zu unterstützt Memory-Mapped IO. Das "Problem" dabei ist jedoch, dass es so ist Implementierung definiert, was einen "flüchtigen Zugriff" darstellt. Und viele Compiler implementieren es so, als wäre die Definition "eine Anweisung, die liest oder Schreiben in den Speicher wurde ausgeführt. "Das ist ein legales, wenn auch nutzlos Definition, ob Die Implementierung spezifiziert es. (Ich muss noch den tatsächlichen finden Spezifikation für jeden Compiler.)

Wohl (und es ist ein Argument, das ich akzeptiere), dies verletzt die Absicht der Standard, da die Hardware die Adressen nicht als Memory-Mapped erkennt IO, und hemmt jede Neuordnung, etc., können Sie nicht flüchtig für Speicher verwenden Mapped IO, zumindest auf Sparc- oder Intel-Architekturen. Trotzdem, nichts davon Die Compiler, die ich angeschaut habe (Sun CC, g ++ und MSC), geben jeden Zaun oder jedes Glied aus Anleitung. (Etwa zu der Zeit, als Microsoft vorschlug, die Regeln für volatileIch denke, einige ihrer Compiler haben ihren Vorschlag umgesetzt und dies auch getan strahlen Zaunanweisungen für flüchtige Zugriffe aus. Ich habe nicht überprüft, was neu ist Compiler tun es, aber es würde mich nicht überraschen, wenn es von einem Compiler abhängig wäre Möglichkeit. Die Version, die ich überprüft habe - ich glaube, es war VS6.0 - hat nicht gesendet Zäune jedoch.)


5
2017-10-12 11:30



Es muss nicht sein. Flüchtig ist kein Synchronisationsgrundelement. Es deaktiviert nur Optimierungen, d. H. Sie erhalten eine vorhersagbare Folge von Lese- und Schreibvorgängen innerhalb eines Threads in der gleichen Reihenfolge wie von der abstrakten Maschine vorgeschrieben. Aber Lese- und Schreibvorgänge in verschiedenen Threads haben überhaupt keine Ordnung, es macht keinen Sinn davon zu sprechen, ihre Ordnung zu erhalten oder nicht zu bewahren. Die Reihenfolge zwischen Theads kann durch Synchronisationsgrundelemente festgelegt werden, Sie erhalten UB ohne sie.

Ein bisschen Erklärung bezüglich der Speicherbarrieren. Eine typische CPU hat mehrere Ebenen des Speicherzugriffs. Es gibt eine Speicher-Pipeline, mehrere Ebenen von Cache, dann RAM usw.

Membar-Anweisungen spülen die Pipeline. Sie ändern nicht die Reihenfolge, in der Lese- und Schreibvorgänge ausgeführt werden, es erzwingt nur, dass die ausstehenden zu einem bestimmten Zeitpunkt ausgeführt werden. Es ist nützlich für Multithread-Programme, aber sonst nicht viel.

Cache (s) sind normalerweise automatisch zwischen den CPUs kohärent. Wenn sichergestellt werden soll, dass der Cache mit dem RAM synchronisiert ist, ist Cache-Flush erforderlich. Es ist sehr verschieden von einem Mitglied.


5
2017-10-13 00:27