Kann der Compiler die Variable, die als flüchtig deklariert wurde, zwischenspeichern

8

Soweit ich weiß, optimiert der Compiler niemals eine Variable, die als volatile deklariert ist. Allerdings habe ich ein Array wie folgt deklariert.

%Vor%

Und verschiedene Threads lesen und schreiben darauf. Ein Element des Arrays wird nur von einem der Threads modifiziert und von einem anderen Thread gelesen. In bestimmten Situationen habe ich jedoch festgestellt, dass selbst wenn ich ein Element aus einem Thread ändere, der Thread, der es liest, die Änderung nicht bemerkt. Es liest den gleichen alten Wert weiter, als hätte der Compiler es irgendwo zwischengespeichert. Aber der Compiler sollte grundsätzlich keine flüchtige Variable zwischenspeichern, richtig? Also, wie kommt es dazu?

HINWEIS: Ich verwende volatile nicht für die Thread-Synchronisation, also bitte hört auf, mir Antworten wie eine Sperre oder eine atomare Variable zu geben. Ich kenne den Unterschied zwischen flüchtigen, atomaren Variablen und Mutexen. Beachten Sie auch, dass die Architektur x86 mit proaktiver Cache-Kohärenz ist. Außerdem lese ich die Variable lange genug, nachdem sie angeblich vom anderen Thread geändert wurde. Auch nach einer langen Zeit kann der Lese-Thread den geänderten Wert nicht sehen.

    
pythonic 03.10.2012, 14:10
quelle

10 Antworten

6
  

Aber der Compiler sollte im Prinzip keine flüchtige Variable zwischenspeichern, richtig?

Nein, der Compiler muss die Adresse der Variablen grundsätzlich lesen / schreiben, wenn Sie die Variable lesen / schreiben.

[Edit: Zumindest muss dies bis zu dem Punkt geschehen, an dem die Implementierung glaubt, dass der Wert an dieser Adresse "beobachtbar" ist. Wie Dietmar in seiner Antwort ausführt, könnte eine Implementierung erklären, dass normales Gedächtnis "nicht beobachtet werden kann". Dies wäre für Leute, die Debugger verwenden, eine Überraschung, mprotect , oder andere Sachen, die außerhalb des Geltungsbereiches des Standards liegen, aber es könnte im Prinzip übereinstimmen.]

In C ++ 03, das überhaupt keine Threads berücksichtigt, muss die Implementierung definieren, was "auf die Adresse zugreifen" bedeutet, wenn sie in einem Thread ausgeführt wird. Details wie diese werden als "Speichermodell" bezeichnet. Pthreads zum Beispiel ermöglicht die Thread-Zwischenspeicherung des gesamten Speichers, einschließlich der flüchtigen Variablen. IIRC, MSVC bietet eine Garantie, dass volatile Variablen geeigneter Größe atomar sind, und es vermeidet Caching (eher wird es bis zu einem einzigen kohärenten Cache für alle Kerne fluten). Der Grund, warum es diese Garantie bietet, ist, dass es bei Intel billig ist - Windows interessiert sich nur für Intel-basierte Architekturen, während Posix sich mit exotischeren Dingen beschäftigt.

C ++ 11 definiert ein Speichermodell für Threading und sagt, dass dies ein Datenrennen ist (dh dass volatile nicht sicherstellt, dass ein Lesevorgang in einem Thread relativ zu a. sequenziert wird schreibe in einen anderen Thread). Zwei Zugriffe können in einer bestimmten Reihenfolge sequenziert werden, in nicht spezifizierter Reihenfolge sequenziert werden (der Standard könnte sagen "unbestimmte Reihenfolge", ich kann mich nicht erinnern) oder überhaupt nicht sequenziert. Nicht sequenziert überhaupt ist schlecht - wenn einer von zwei nicht-sequenzierten Zugriffen ein Schreiben ist, dann ist das Verhalten undefiniert.

Der Schlüssel hier ist das implizierte "und dann" in "Ich modifiziere ein Element aus einem Thread und dann bemerkt der Thread, der es liest, die Änderung nicht". Sie gehen davon aus, dass die Vorgänge sequenziert sind, aber nicht. Soweit es den Lese-Thread betrifft, ist der Schreibvorgang in dem anderen Thread nicht unbedingt erforderlich, es sei denn, Sie verwenden eine Art von Synchronisation. Und eigentlich ist es schlimmer als das - Sie könnten von dem, was ich gerade geschrieben habe, denken, dass nur die Reihenfolge der Operationen nicht spezifiziert ist, aber das Verhalten eines Programms mit einem Datenrennen ist undefiniert.

    
Steve Jessop 03.10.2012, 14:20
quelle
3

C

Was flüchtig ist:

  • Garantiert einen aktuellen Wert in der Variablen, wenn die Variable von einer externen Quelle (einem Hardware-Register, einem Interrupt, einem anderen Thread, einer Callback-Funktion usw.) geändert wird.
  • Blockiert alle Optimierungen des Lese- / Schreibzugriffs auf die Variable.
  • Verhindern Sie gefährliche Optimierungsfehler, die bei Variablen auftreten können, die zwischen mehreren Threads / Interrupts / Callback-Funktionen geteilt werden, wenn der Compiler nicht erkennt, dass der Thread / Interrupt / Callback vom Programm aufgerufen wird. (Dies ist besonders häufig bei verschiedenen fragwürdigen Embedded-System-Compilern der Fall, und wenn Sie diesen Fehler bekommen, ist es sehr schwierig, ihn zu finden.)

Was flüchtig ist nicht:

  • Es garantiert keinen atomaren Zugriff oder irgendeine Form von Thread-Sicherheit.
  • Es kann nicht anstelle eines Mutex / Semaphor / Guard / kritischen Abschnitts verwendet werden. Es kann nicht für die Thread-Synchronisierung verwendet werden.

Was flüchtig ist oder nicht:

  • Es kann von dem Compiler implementiert werden oder nicht, um eine Speicherbarriere bereitzustellen, um in einer Umgebung mit mehreren Kernen vor Instruktions-Cache- / Anweisungs-Pipe / Befehl-Umordnungsproblemen zu schützen. Sie sollten niemals davon ausgehen, dass volatile dies für Sie übernimmt, es sei denn, die Compiler-Dokumentation besagt explizit, dass dies der Fall ist.
Lundin 03.10.2012 14:44
quelle
2

Mit volatile können Sie nur festlegen, dass eine Variable erneut gelesen wird, wenn Sie ihren Wert verwenden. Es garantiert nicht, dass die verschiedenen Werte / Repräsentationen, die auf verschiedenen Ebenen Ihrer Architektur vorhanden sind, konsistent sind.

Um solche Garantien zu haben, brauchen Sie die neuen Dienstprogramme von C11 und C ++ 1 bezüglich atomarer Zugriffe und Speicherbarrieren. Viele Compiler implementieren diese bereits in Bezug auf die Erweiterung. Zum Beispiel haben die gcc-Familie (clang, icc, etc) eingebaute mit dem Präfix __sync , um diese zu implementieren.

    
Jens Gustedt 03.10.2012 14:17
quelle
2

Für C ++:

  

Soweit ich weiß, optimiert der Compiler niemals eine Variable, die als flüchtig deklariert ist.

Ihre Prämisse ist falsch. volatile ist ein Hinweis auf den Compiler und garantiert eigentlich nichts. Compiler können einige Optimierungen für volatile Variablen verhindern, aber das war's.

volatile ist keine Sperre, versuchen Sie nicht, sie als solche zu verwenden.

7.1.5.1

  

7) [Hinweis: volatile ist ein Hinweis auf die zu vermeidende Implementierung   aggressive Optimierung mit dem Objekt, weil der Wert der   Das Objekt kann durch eine nicht implementierbare Einrichtung geändert werden.   Eine detaillierte Semantik finden Sie in 1.9. Im Allgemeinen ist die Semantik von flüchtigen   sollen in C ++ gleich sein wie in C.-Endnote]

    
Luchian Grigore 03.10.2012 14:12
quelle
2

Das Schlüsselwort volatile hat nichts mit der Parallelität in C ++ überhaupt zu tun! Es wird verwendet, um zu verhindern, dass der Compiler den vorherigen Wert verwendet, d. H. Der Compiler erzeugt bei jedem Zugriff auf den Code einen Code, der auf einen volatile -Wert zugreift. Der Hauptzweck sind Dinge wie Memory-Mapped I / O. Die Verwendung von volatile hat jedoch no Auswirkungen auf das, was die CPU beim Lesen von normalem Speicher tut: Wenn die CPU keinen Grund zu der Annahme hat, dass sich der Wert im Speicher geändert hat, z. B. weil keine Synchronisationsanweisung vorhanden ist Es kann nur den Wert aus seinem Cache verwenden. Um zwischen den Threads zu kommunizieren, benötigen Sie eine Synchronisation, z. B. std::atomic<T> , lock a std::mutex , etc.

    
Dietmar Kühl 03.10.2012 14:25
quelle
1

Volatile Schlüsselwort garantiert nur, dass der Compiler kein Register für diese Variable verwendet. Somit wird jeder Zugriff auf diese Variable gehen und den Speicherort lesen. Jetzt nehme ich an, dass Sie Cache-Kohärenz zwischen den mehreren Prozessoren in Ihrer Architektur haben. Wenn also ein Prozessor schreibt und andere es liest, sollte es unter normalen Bedingungen sichtbar sein. Sie sollten jedoch die Eckfälle berücksichtigen. Angenommen, die Variable befindet sich in der Pipeline eines Prozessorkerns, und ein anderer Prozessor versucht, sie zu lesen, vorausgesetzt, dass sie geschrieben wurde, dann liegt ein Problem vor. Im Wesentlichen sollten die geteilten Variablen entweder durch Sperren geschützt werden oder sollten durch den Barrieremechanismus korrekt geschützt werden.

    
Raj 03.10.2012 14:22
quelle
1

Volatile wirkt sich nur auf die Variable aus, vor der es liegt. Hier in Ihrem Beispiel ein Zeiger. Ihr Code: volatile long array [8], der Zeiger auf das erste Element des Arrays ist flüchtig, nicht der Inhalt. (Gleiches gilt für Objekte jeglicher Art)

Sie könnten es wie in anpassen Wie deklariere ich Ein Array, das mit malloc erstellt wurde, um in C ++ flüchtig zu sein

    
user3387542 06.03.2014 10:02
quelle
0
  

In bestimmten Situationen habe ich jedoch festgestellt, dass selbst wenn ich ein modifiziere   Element aus einem Thread, der Thread liest es nicht bemerkt   Veränderung. Es liest den gleichen alten Wert, als hätte der Compiler   irgendwo zwischengespeichert.

Dies liegt nicht daran, dass der Compiler sie irgendwo zwischengespeichert hat, sondern weil der Lese-Thread aus dem Cache seines CPU-Kerns liest, der sich möglicherweise von dem des schreibenden Threads unterscheidet. Um die Weitergabe von Wertänderungen über CPU-Kerne sicherzustellen, müssen Sie geeignete Speicherzäune verwenden, und in C ++ können Sie dafür weder flüchtig noch flüchtig sein.

    
usta 03.10.2012 14:23
quelle
0

Die Semantik von volatile ist implementierungsdefiniert. Wenn ein Compiler wüsste, dass Interrupts während der Ausführung bestimmter Codeabschnitte deaktiviert wären, und wüsste, dass es auf der Zielplattform andere Mittel als Interrupt-Handler geben würde, über die Vorgänge auf einem bestimmten Speicher beobachtbar wären, könnte er -cache volatile -qualifizierte Variablen in einem solchen Speicher genau so, wie sie gewöhnliche Variablen zwischenspeichern könnten, vorausgesetzt, sie dokumentierten ein solches Verhalten.

Beachten Sie, dass einige Aspekte des Verhaltens als "beobachtbar" gewertet werden und in gewissem Maße durch die Implementierung definiert werden können. Wenn eine Implementierung dokumentiert, dass sie nicht für die Verwendung auf Hardware gedacht ist, die Haupt-RAM-Zugriffe verwendet, um erforderliche extern sichtbare Aktionen auszulösen, dann wären Zugriffe auf das Haupt-RAM bei dieser Implementierung nicht "beobachtbar". Die Implementierung wäre mit Hardware kompatibel, die solche Zugriffe physisch beobachten könnte, wenn es nicht darauf ankommt, ob solche Zugriffe tatsächlich gesehen werden. Waren solche Zugriffe jedoch erforderlich, wie dies der Fall wäre, wenn die Zugriffe als "beobachtbar" galten, würde der Compiler keine Kompatibilität beanspruchen und somit nichts versprechen.

    
supercat 19.02.2017 00:01
quelle
0

C ++ - Zugriffe durch flüchtige L-Werte und C-Zugriffe auf flüchtige Objekte sind "abstrakt" "beobachtbar" - obwohl in der Praxis Das C-Verhalten entspricht dem C ++ - Standard und nicht dem C-Standard. Informell teilt die Deklaration volatile jedem -Thread mit, dass sich der Wert irgendwie ändern könnte, unabhängig vom Text in jedem -Thread. Unter den Standards mit Threads gibt es keine Vorstellung von einem Schreiben durch einen anderen Thread, der eine Änderung in einem Objekt verursacht, flüchtig oder nicht, geteilt oder nicht, außer für eine gemeinsam genutzte Variable durch den Synchronisierungsfunktionsaufruf zu Beginn eines synchronisierten kritischen Objekts Region. volatile ist irrelevant , um gemeinsam genutzte Objekte einzufügen.

Wenn Ihr Code den Thread, über den Sie sprechen, nicht richtig synchronisiert, hat Ihr einziger Thread, der liest, was ein anderer Thread geschrieben hat, ein undefiniertes Verhalten. Der Compiler kann also beliebigen Code generieren. Wenn Ihr Code ordnungsgemäß synchronisiert ist, werden Schreibvorgänge durch andere Threads nur bei Threadsynchronisierungsaufrufen ausgeführt. Sie brauchen dafür nicht volatile .

PS

Die Standards sagen "Was einen Zugang zu einem Objekt ausmacht, das Ist der Typ flüchtig qualifiziert, ist die Implementierung definiert. "Sie können also nicht einfach davon ausgehen, dass für jede Dereferenzierung eines flüchtigen L-Werts oder Schreibzugriffs für jede Zuweisung ein Lesezugriff besteht.

Außerdem ist die Implementierung (definiert als "abstrakt") "beobachtbar" volatile Zugriffe "tatsächlich" manifestiert. Ein Compiler generiert daher möglicherweise keinen Code für Hardwarezugriffe, die den definierten abstrakten Zugriffen entsprechen. ZB können nur Objekte mit statischer Speicherdauer und externer Verknüpfung, die mit einem bestimmten Flag zum Verknüpfen in spezielle Hardware-Positionen kompiliert wurden, von außerhalb des Programmtextes verändert werden, so dass% ce_de% anderer Objekte ignoriert wird.

    
philipxy 20.02.2017 06:49
quelle