Nach dem Blick auf ein Bündel von anderen Fragen und ihre Antworten , habe ich den Eindruck , dass es auf das, was die „flüchtig“ Schlüsselwort in C genau bedeutet keine weitgehende Übereinstimmung ist.
Selbst der Standard selbst scheint nicht klar genug zu sein, damit sich alle einig sind was er bedeutet .
Unter anderem:
- Es scheint je nach Hardware und Compiler unterschiedliche Garantien zu bieten.
- Dies wirkt sich auf Compiler-Optimierungen aus, nicht jedoch auf Hardware-Optimierungen. Bei einem erweiterten Prozessor, der seine eigenen Laufzeitoptimierungen durchführt, ist nicht einmal klar, ob der Compiler die von Ihnen gewünschte Optimierung verhindern kann. (Einige Compiler generieren Anweisungen, um einige Hardwareoptimierungen auf einigen Systemen zu verhindern. Dies scheint jedoch in keiner Weise standardisiert zu sein.)
Um das Problem zusammenzufassen, scheint (nach vielem Lesen) "volatile" etwas zu garantieren wie: Der Wert wird nicht nur aus / in ein Register, sondern zumindest in den L1-Cache des Kerns in derselben Reihenfolge gelesen / geschrieben Die Lese- / Schreibvorgänge werden im Code angezeigt. Dies scheint jedoch nutzlos zu sein, da das Lesen / Schreiben von / in ein Register innerhalb desselben Threads bereits ausreicht, während die Koordination mit dem L1-Cache keine weiteren Garantien hinsichtlich der Koordination mit anderen Threads garantiert. Ich kann mir nicht vorstellen, wann es jemals wichtig sein könnte, nur mit dem L1-Cache zu synchronisieren.
VERWENDE 1
Die einzige allgemein vereinbarte Verwendung von flüchtig scheint für alte oder eingebettete Systeme zu sein, bei denen bestimmte Speicherorte Hardware-E / A-Funktionen zugeordnet sind, wie z. B. ein Bit im Speicher, das (direkt in der Hardware) ein Licht steuert oder ein bisschen im Speicher, das Ihnen sagt, ob eine Tastaturtaste gedrückt ist oder nicht (weil sie von der Hardware direkt mit der Taste verbunden ist).
Es scheint, dass "use 1" nicht in portablem Code vorkommt, dessen Ziele Mehrkernsysteme umfassen.
USE 2
Nicht zu verschieden von "use 1" ist der Speicher, der jederzeit von einem Interrupt-Handler gelesen oder geschrieben werden kann (der möglicherweise ein Licht steuert oder Informationen von einem Schlüssel speichert). Aber schon deshalb haben wir das Problem, dass der Interrupt-Handler je nach System auf einem anderen Kern mit eigenem Speicher-Cache ausgeführt wird und "volatile" nicht die Cache-Kohärenz auf allen Systemen garantiert.
So „use 2“ scheint jenseits dessen, was „flüchtig“ liefern zu können.
VERWENDUNG 3
Die einzige andere unbestrittene Verwendung, die ich sehe, besteht darin, eine Fehloptimierung von Zugriffen über verschiedene Variablen zu verhindern, die auf denselben Speicher verweisen, von dem der Compiler nicht erkennt, dass er denselben Speicher hat. Aber das ist wahrscheinlich nur unbestritten, weil die Leute nicht darüber reden - ich habe nur eine Erwähnung davon gesehen. Und ich dachte, der C-Standard hat bereits erkannt, dass "verschiedene" Zeiger (wie verschiedene Argumente auf eine Funktion) auf dasselbe Element oder auf Elemente in der Nähe verweisen können, und bereits angegeben, dass der Compiler Code erzeugen muss, der auch in solchen Fällen funktioniert. Ich konnte dieses Thema jedoch im neuesten Standard (500 Seiten!) Nicht schnell finden.
Also "use 3" gibt es vielleicht gar nicht ?
Daher meine Frage:
Garantiert "flüchtig" überhaupt etwas im tragbaren C-Code für Mehrkernsysteme?
EDIT - Update
Nach dem Durchsuchen des neuesten Standards sieht es so aus, als ob die Antwort zumindest ein sehr begrenztes Ja ist:
1. Der Standard legt wiederholt eine spezielle Behandlung für den spezifischen Typ "volatile sig_atomic_t" fest. Der Standard besagt jedoch auch, dass die Verwendung der Signalfunktion in einem Multithread-Programm zu undefiniertem Verhalten führt. Daher scheint dieser Anwendungsfall auf die Kommunikation zwischen einem Single-Thread-Programm und seinem Signalhandler beschränkt zu sein.
2. Der Standard gibt auch eine klare Bedeutung für "flüchtig" in Bezug auf setjmp / longjmp an. (Beispielcode, wo es darauf ankommt, ist in anderen Fragen und Antworten angegeben .)
Die genauere Frage lautet also:
Garantiert "flüchtig" überhaupt etwas im tragbaren C-Code für Mehrkernsysteme, abgesehen von (1) dem Empfangen von Informationen von einem Single-Thread-Programm von seinem Signalhandler oder (2) dem Zulassen von setjmp Code, um Variablen zu sehen, die zwischen setjmp und longjmp geändert wurden?
Dies ist immer noch eine Ja / Nein-Frage.
Wenn "Ja", wäre es großartig, wenn Sie ein Beispiel für fehlerfreien tragbaren Code zeigen könnten, der fehlerhaft wird, wenn "flüchtig" weggelassen wird. Wenn "nein", kann ein Compiler "volatile" außerhalb dieser beiden sehr spezifischen Fälle für Multi-Core-Ziele ignorieren.
volatile
, um das Programm darüber zu informieren, dass es sich asynchron ändern kann.volatile
speziell um das, was ich für notwendig halte.Antworten:
Nein, das tut es absolut nicht . Und das macht volatile für den Zweck des MT-Safe-Codes fast unbrauchbar.
Wenn dies der Fall ist, ist flüchtig für Variablen, die von mehreren Threads gemeinsam genutzt werden, sehr gut geeignet, da die Reihenfolge der Ereignisse im L1-Cache alles ist, was Sie in einer typischen CPU (dh Multi-Core oder Multi-CPU auf dem Motherboard) tun müssen, die zusammenarbeiten kann auf eine Weise, die eine normale Implementierung von C / C ++ - oder Java-Multithreading mit typischen erwarteten Kosten ermöglicht (dh keine großen Kosten für die meisten atomaren oder nicht zufriedenen Mutex-Operationen).
Aber volatil nicht weder theoretisch noch in der Praxis eine garantierte Reihenfolge (oder "Speichersichtbarkeit") im Cache.
(Hinweis: Das Folgende basiert auf einer fundierten Interpretation der Standarddokumente, der Absicht des Standards, der historischen Praxis und einem tiefen Verständnis der Erwartungen von Compiler-Autoren. Dieser Ansatz basiert auf der Geschichte, den tatsächlichen Praktiken sowie den Erwartungen und dem Verständnis realer Personen in die reale Welt, die viel stärker und zuverlässiger ist als das Parsen der Wörter eines Dokuments, von dem nicht bekannt ist, dass es sich um herausragende Spezifikationen handelt und das viele Male überarbeitet wurde.)
In der Praxis garantiert volatile eine ptrace-Fähigkeit, dh die Fähigkeit, Debug-Informationen für das laufende Programm auf jeder Optimierungsstufe zu verwenden , und die Tatsache, dass die Debug-Informationen für diese flüchtigen Objekte sinnvoll sind:
ptrace
(einen ptrace-ähnlichen Mechanismus) verwenden, um sinnvolle Haltepunkte an den Sequenzpunkten nach Operationen mit flüchtigen Objekten festzulegen: Sie können wirklich genau an diesen Punkten brechen (beachten Sie, dass dies nur funktioniert, wenn Sie bereit sind, viele Haltepunkte als solche festzulegen Die C / C ++ - Anweisung kann zu vielen verschiedenen Start- und Endpunkten der Assembly kompiliert werden (wie in einer massiv abgewickelten Schleife).Volatile Garantie in der Praxis etwas mehr als die strikte ptrace-Interpretation: Sie garantiert auch, dass flüchtige automatische Variablen eine Adresse auf dem Stapel haben, da sie keinem Register zugeordnet sind, eine Registerzuordnung, die ptrace-Manipulationen empfindlicher machen würde (Compiler kann Debug-Informationen ausgeben, um zu erklären, wie Variablen Registern zugewiesen werden, aber das Lesen und Ändern des Registerstatus ist etwas aufwändiger als der Zugriff auf Speicheradressen.
Beachten Sie, dass die vollständige Programm-Debug-Fähigkeit, bei der alle Variablen zumindest an Sequenzpunkten flüchtig sind, durch den "Null-Optimierungs" -Modus des Compilers bereitgestellt wird, der immer noch triviale Optimierungen wie arithmetische Vereinfachungen durchführt (normalerweise gibt es keine garantierte Nein-Option) Optimierung in allen Modi). Flüchtig ist jedoch stärker als nicht optimiert:
x-x
Kann für eine nichtflüchtige Ganzzahl,x
jedoch nicht für ein flüchtiges Objekt vereinfacht werden.So flüchtige Mittel, die garantiert so kompiliert werden, wie sie sind , wie die Übersetzung eines Systemaufrufs von der Quelle in die Binärdatei / Assembly durch den Compiler keine Neuinterpretation, Änderung oder Optimierung durch einen Compiler. Beachten Sie, dass Bibliotheksaufrufe Systemaufrufe sein können oder nicht. Viele offizielle Systemfunktionen sind tatsächlich Bibliotheksfunktionen, die eine dünne Interpositionsschicht bieten und sich am Ende im Allgemeinen auf den Kernel verschieben. (Insbesondere
getpid
muss nicht zum Kernel gegangen werden und könnte einen Speicherort lesen, der vom Betriebssystem bereitgestellt wird, der die Informationen enthält.)Flüchtige Wechselwirkungen sind Wechselwirkungen mit der Außenwelt der realen Maschine , die der "abstrakten Maschine" folgen müssen. Sie sind keine internen Interaktionen von Programmteilen mit anderen Programmteilen. Der Compiler kann nur über das nachdenken, was er weiß, das sind die internen Programmteile.
Die Codegenerierung für einen flüchtigen Zugriff sollte der natürlichsten Interaktion mit diesem Speicherort folgen: Es sollte nicht überraschend sein. Das bedeutet, dass einige flüchtige Zugriffe atomar sein sollen : Wenn die natürliche Art, die Darstellung von a
long
in der Architektur zu lesen oder zu schreiben, atomar ist, wird erwartet, dass ein Lese- oder Schreibvorgang von avolatile long
atomar ist, da der Compiler nicht generieren sollte alberner ineffizienter Code, um beispielsweise byteweise auf flüchtige Objekte zuzugreifen .Sie sollten dies feststellen können, indem Sie die Architektur kennen. Sie müssen nichts über den Compiler wissen, da flüchtig bedeutet, dass der Compiler transparent sein sollte .
Volatile erzwingt jedoch nur die Emission der erwarteten Assembly für die für bestimmte Fälle am wenigsten optimierten, um eine Speicheroperation auszuführen: Volatile Semantik bedeutet allgemeine Fallsemantik.
Der allgemeine Fall ist, was der Compiler tut, wenn er keine Informationen über ein Konstrukt hat: f.ex. Das Aufrufen einer virtuellen Funktion für einen Wert über den dynamischen Versand ist ein allgemeiner Fall, bei dem der Overrider direkt aufgerufen wird, nachdem zur Kompilierungszeit der Typ des durch den Ausdruck angegebenen Objekts bestimmt wurde. Der Compiler hat immer eine allgemeine Fallbehandlung aller Konstrukte und folgt dem ABI.
Volatile unternimmt nichts Besonderes, um Threads zu synchronisieren oder "Speichersichtbarkeit" bereitzustellen: Volatile bietet nur Garantien auf abstrakter Ebene , die von einem Thread aus ausgeführt oder gestoppt werden, dh innerhalb eines CPU-Kerns :
Nur der zweite Punkt bedeutet, dass flüchtig bei den meisten Kommunikationsproblemen zwischen Threads nicht nützlich ist. Der erste Punkt ist bei Programmierproblemen, bei denen keine Kommunikation mit Hardwarekomponenten außerhalb der CPU (s), aber immer noch auf dem Speicherbus erfolgt, im Wesentlichen irrelevant.
Die Eigenschaft von flüchtig, ein garantiertes Verhalten aus der Sicht des Kerns bereitzustellen, der den Thread ausführt, bedeutet, dass an diesen Thread gelieferte asynchrone Signale, die unter dem Gesichtspunkt der Ausführungsreihenfolge dieses Threads ausgeführt werden, Operationen in der Quellcodereihenfolge sehen .
Wenn Sie nicht vorhaben, Signale an Ihre Threads zu senden (ein äußerst nützlicher Ansatz zur Konsolidierung von Informationen über aktuell ausgeführte Threads ohne zuvor vereinbarten Stopppunkt), ist volatile nichts für Sie.
quelle
Ich bin kein Experte, aber cppreference.com hat einige meiner Meinung nach ziemlich gute Informationen
volatile
. Hier ist der Kern davon:Es gibt auch einige Verwendungszwecke:
Und natürlich wird erwähnt, dass
volatile
dies für die Thread-Synchronisation nicht nützlich ist:quelle
longjmp
in C ++ - Code verwenden.Erstens gab es historisch gesehen verschiedene Probleme mit unterschiedlichen Interpretationen der Bedeutung von
volatile
Zugang und ähnlichem. Sehen Sie diese Studie: Flüchtige Bestandteile werden Miscompiled, und was zu tun ist über sie .Abgesehen von den verschiedenen in dieser Studie erwähnten Problemen ist das Verhalten von
volatile
tragbar, abgesehen von einem Aspekt: Wenn sie als Speicherbarrieren fungieren . Eine Speicherbarriere ist ein Mechanismus, der die gleichzeitige Ausführung Ihres Codes ohne Sequenz verhindert. Die Verwendungvolatile
als Speicherbarriere ist sicherlich nicht tragbar.Ob die C-Sprache das Gedächtnisverhalten garantiert oder nicht,
volatile
ist anscheinend fraglich, obwohl ich persönlich denke, dass die Sprache klar ist. Zuerst haben wir die formale Definition von Nebenwirkungen, C17 5.1.2.3:Der Standard definiert den Begriff Sequenzierung als eine Möglichkeit, die Reihenfolge der Bewertung (Ausführung) zu bestimmen. Die Definition ist formal und umständlich:
Die TL; DR der obigen ist im Grunde genommen, dass, wenn wir einen Ausdruck haben,
A
der Nebenwirkungen enthält, er vor einem anderen Ausdruck ausgeführt werden mussB
, falls er danachB
sequenziert wirdA
.Optimierungen des C-Codes werden durch diesen Teil ermöglicht:
Dies bedeutet, dass das Programm Ausdrücke in der Reihenfolge auswerten (ausführen) kann, die der Standard an anderer Stelle vorschreibt (Reihenfolge der Auswertung usw.). Es muss jedoch keinen Wert auswerten (ausführen), wenn daraus geschlossen werden kann, dass er nicht verwendet wird. Beispielsweise muss die Operation
0 * x
nicht ausgewertet werdenx
den Ausdruck und einfach durch ersetzen0
.Es sei denn, der Zugriff auf eine Variable ist ein Nebeneffekt. Was bedeutet , dass für den Fall
x
istvolatile
, es muss (execute) zu bewerten ,0 * x
auch wenn das Ergebnis ist immer 0. Die Optimierung wird ist nicht erlaubt.Darüber hinaus spricht der Standard von beobachtbarem Verhalten:
In Anbetracht all dessen kann eine konforme Implementierung (Compiler + zugrunde liegendes System) den Zugriff auf
volatile
Objekte möglicherweise nicht in einer nicht sequenzierten Reihenfolge ausführen, falls die Semantik der geschriebenen C-Quelle etwas anderes sagt.Dies bedeutet, dass in diesem Beispiel
Beide Zuweisungsausdrücke müssen ausgewertet werden und
z = x;
müssen vorher ausgewertet werdenz = y;
. Eine Multiprozessor-Implementierung, die diese beiden Operationen an zwei verschiedene Unsequenz-Kerne auslagert, ist nicht konform!Das Dilemma besteht darin, dass Compiler nicht viel gegen Dinge wie Pre-Fetch-Caching und Anweisungs-Pipelining usw. tun können, insbesondere nicht, wenn sie auf einem Betriebssystem ausgeführt werden. Und so übergeben Compiler dieses Problem an die Programmierer und sagen ihnen, dass Speicherbarrieren nun in der Verantwortung des Programmierers liegen. Während der C-Standard eindeutig festlegt, dass das Problem vom Compiler gelöst werden muss.
Der Compiler kümmert sich jedoch nicht unbedingt darum, das Problem zu lösen, und
volatile
ist daher nicht portierbar, um als Speicherbarriere zu fungieren. Es ist zu einem Problem der Qualität der Implementierung geworden.quelle
z
wirklich ausgeführt werden? (wiez = x; z = y;
) Der Wert wird in der nächsten Anweisung gelöscht.z
wirklich zweimal vergeben? Woher wissen Sie, dass "Lesevorgänge ausgeführt werden"?