2013-02-09 5 views
10

Betrachten Sie die folgende Sequenz von Schreibvorgängen zu volatile Speicher, die ich von David Chisnall's article at InformIT genommen habe, „C11 Verständnis und C 11 ++ Atomics“:Müssen Zugriffe auf flüchtige Stoffe neu geordnet werden?

volatile int a = 1; 
volatile int b = 2; 
      a = 3; 

Mein Verständnis von C++ 98 war, dass diese Operationen konnte nicht mehr nachbestellt werden pro C++ 98 1.9:

konforme Implementierungen erforderlich sind (nur) das beobachtbare Verhalten der abstrakten Maschine zu emulieren wie unten ... das beobachtbare Verhalten der erklärt abstrakte Maschine ist die Folge von liest und schreibt auf flüchtige Daten und Anrufe Bibliothek I/O-Funktionen

Chisnall sagt, dass die Beschränkung auf, um die Bewahrung nur auf einzelne Variablen gilt, Schreiben, dass eine konforme Implementierung Code erzeugen könnte, die dies bedeutet:

a = 1; 
a = 3; 
b = 2; 

Oder diese:

b = 2; 
a = 1; 
a = 3; 

C++ 11 wiederholt die C++ 98 Formulierung, dass

konforme Implementierungen sind erforderlich, um (nur) das beobachtbare Verhalten der abstrakten Maschine zu emulieren, wie unten unter erläutert.

aber sagt über volatile s (1,9/8):

Zugriff auf flüchtige Objekte sind streng nach den Regeln der abstrakten Maschine ausgewertet.

1,9/12 sagt, dass ein volatile glvalue Zugriff ist eine Nebenwirkung, und 1,9/14, so dass die Nebenwirkungen in einem vollen Ausdruck (der die Variablen a, b und c oben enthält) (zB ein Anweisung) muss den Nebenwirkungen eines späteren vollständigen Ausdrucks im selben Thread vorausgehen. Dies führt mich zu dem Schluss, dass die zwei Umordnungen, die Chisnall zeigt, ungültig sind, weil sie nicht der Reihenfolge entsprechen, die von der abstrakten Maschine diktiert wird.

Bin ich etwas übersehen, oder ist Chisnall falsch?

(Beachten Sie, dass dies kein Einfädeln Frage. Die Frage, ob ein Compiler in einem einzigen Thread Zugriffe auf unterschiedliche volatile Variablen neu zu ordnen. Erlaubt)

+0

mögliche Duplikate von ["volatile" Qualifier und Compiler-Umordnungen] (http://stackoverflow.com/questions/2535148/volatile-qualifier-and-compiler-reorderings) –

Antwort

5

IMO Chisnalls Interpretation (wie von Ihnen vorgestellt) ist eindeutig falsch. Der einfachere Fall ist C++ 98. Die sequence of reads and writes to volatile data muss beibehalten werden und das gilt für die geordnete Reihenfolge der Lese- und Schreibvorgänge von flüchtigen Daten und nicht für eine einzelne Variable.

Dies wird offensichtlich, wenn Sie die ursprüngliche Motivation für volatile: Memory-Mapped I/O betrachten. In mmio haben Sie typischerweise mehrere verwandte Register an verschiedenen Speicherorten und das Protokoll eines I/O-Geräts erfordert eine spezifische Sequenz von Lese- und Schreibvorgängen in seinem Satz von Registern - die Reihenfolge zwischen den Registern ist wichtig.

Die C++ 11 Formulierung vermeidet sprechen über eine absolute sequence of reads and writes, weil in Multi-Threaded-Umgebungen gibt es nicht eine einzige wohldefinierte Sequenz solcher Ereignisse über Threads - und das ist kein Problem, wenn diese Zugriffe gehen zu unabhängige Speicherorte. Aber ich glaube, die Absicht ist, dass für jede Folge flüchtiger Datenzugriffe mit einer wohldefinierten Reihenfolge die Regeln die gleichen bleiben wie für C++ 98 - die Reihenfolge muss beibehalten werden, egal wie viele verschiedene Orte in dieser Sequenz aufgerufen werden.

Es ist ein völlig separates Thema, was dies für eine Implementierung bedeutet. Wie (und selbst wenn) ein flüchtiger Datenzugriff von außerhalb des Programms beobachtbar ist und wie die Zugriffsreihenfolge des Programms auf extern beobachtbare Ereignisse abgebildet ist, ist nicht spezifiziert. Eine Implementierung sollte Ihnen wahrscheinlich eine vernünftige Interpretation und angemessene Garantien geben, aber was sinnvoll ist, hängt vom Kontext ab.

Der C++ 11-Standard lässt Platz für Datenrennen zwischen unsynchronisierten flüchtigen Zugriffen, es ist also nichts erforderlich, das diese durch vollständige Speicherzäune oder ähnliche Konstrukte umgibt. Wenn es Teile des Speichers gibt, die wirklich als externe Schnittstelle verwendet werden - für Memory-Mapped I/O oder DMA - dann kann es sinnvoll sein, dass die Implementierung Ihnen Garantien gibt, wie flüchtige Zugriffe auf diese Teile den konsumierenden Geräten ausgesetzt sind.

Eine Garantie kann wahrscheinlich aus dem Standard abgeleitet werden (siehe [in.execution]): Werte vom Typ volatile std::sigatomic_t müssen Werte haben, die mit der Reihenfolge der Schreibvorgänge auch in einem Signalhandler kompatibel sind - zumindest in einem single-threaded Programm.

+1

Ich denke deine Antwort ist selbst widerlegend. Dein vierter Absatz widerlegt deine erste. Chisnall sagt, dass Sie sich nicht portabel auf die C++ - Standardbestellung "Garantie" verlassen können und das, was Ihr vierter Absatz sagt. Aber dein erster Absatz sagt, dass er falsch liegt. Es gibt einfach kein * tragbares * Konzept einer "Abfolge von Lese- und Schreibvorgängen für flüchtige Daten", daher gibt es keine universelle semantische Bedeutung für die Garantie des Standards. –

+1

@David Schwartz: Ich denke, du hast meinen Punkt verpasst. Chisnall sagt, dass Speicher für verschiedene flüchtige Stoffe nachbestellt werden können (selbst wenn sie beobachtbar sind), aber das Verschmelzen in einen einzigen flüchtigen Stoff ist nicht erlaubt. Ich sage, dass beides nicht erlaubt ist, wenn es (in einer implementierungsdefinierten Weise) beobachtet werden kann. Ich sage auch, dass es keine Garantie gibt, dass alle Verwendungen von "volatilen" qualifizierten Variablen beobachtet werden können oder eine beobachtbare Speicherreihenfolge haben. Zusammenfassend: Wenn die Geschäfte oder ihre Reihenfolge nicht eingehalten werden kann, ist Chisnalls "erlaubte Neuordnung" bedeutungslos; Wenn sie beobachtet werden können, ist es falsch. – JoergB

+0

Wenn es keine tragbare Idee gibt, Lese- und Schreibvorgänge auf flüchtige Substanzen zu beobachten, dann bedeutet der Standard, der sie zum beobachtbaren Verhalten des Programms macht, nichts. Wenn es bedeutet, was auch immer für diese Plattform sinnvoll ist, kann sich portabler Code nicht darauf verlassen, irgendetwas zu tun. Insbesondere wenn Sie versuchen, diese Logik darauf anzuwenden, ob z. B. Speicherzäune um flüchtige Elemente herum benötigt werden, kommen Sie zurück auf die Frage, ob CPU-Neuordnung beobachtbar ist, und es gibt keine eindeutige Antwort darauf. Selbst wenn wir mit Ihnen übereinstimmen, würde es bei keiner der Fragen helfen, die wir beantworten möchten. –

0

es sieht aus wie passieren kann.

Es gibt eine Diskussion auf dieser Seite:

http://gcc.gnu.org/ml/gcc/2003-11/msg01419.html

+2

Das ist eine Diskussion darüber, was im generierten Code zu tun ist um ein solches Neuordnen in der Hardware zu verhindern. Meine Frage ist, was der C++ 11-Standard vorschreibt. Es liegt dann an den Compiler-Anbietern, sicherzustellen, dass der von ihnen erzeugte Code verhindert, dass die Hardware unzulässige Umordnungen durchführt. – KnowItAllWannabe

-2

C 98 ++ nicht in den Anweisungen nicht nachbestellt werden.

Das beobachtbare Verhalten der abstrakten Maschine ist die Folge von liest und schreibt auf flüchtige Daten und ruft Bibliothek I/O-Funktionen

Dieses sagt, es ist die tatsächliche Abfolge der liest und schreibt selbst, nicht die Anweisungen, die sie erzeugen. Jedes Argument, das besagt, dass die Anweisungen die Lese- und Schreibvorgänge in der Programmreihenfolge widerspiegeln müssen, könnte ebenso argumentieren, dass das Lesen und Schreiben in den RAM selbst in der Programmreihenfolge erfolgen muss, und dies ist eindeutig eine absurde Interpretation der Anforderung.

Einfach gesagt, das bedeutet nichts. Es gibt keinen "richtigen Platz", um die Lese- und Schreibreihenfolge zu beobachten (Der RAM-Bus? Der CPU-Bus? Zwischen den L1- und L2-Caches? Von einem anderen Thread? Von einem anderen Kern?), Also ist diese Anforderung im Wesentlichen bedeutungslos.

Versionen von C++ vor allen Verweisen auf Threads geben eindeutig nicht das Verhalten von volatilen Variablen wie aus einem anderen Thread angezeigt. Und C++ 11 (weise, IMO) didn't change this, sondern stattdessen sinnvolle atomare Operationen mit wohldefinierter Inter-Thread-Semantik eingeführt.

Bei Speicherkarten-Hardware wird das immer plattformspezifisch sein. Der C++ - Standard gibt nicht einmal vor, wie dies richtig gehandhabt werden könnte. Zum Beispiel könnte die Plattform so sein, dass nur eine Teilmenge von Speicheroperationen in diesem Kontext legal ist, zum Beispiel diejenigen, die einen Schreib-Buchungspuffer umgehen, der neu geordnet werden kann, und der C++ - Standard zwingt den Compiler sicherlich nicht dazu, die richtigen Anweisungen auszusenden das bestimmte Hardware-Gerät - wie könnte es?

Update: Ich sehe einige downvotes, weil Leute diese Wahrheit nicht mögen. Leider ist es wahr. Wenn der C++ - Standard es dem Compiler verbietet, Zugriffe auf verschiedene flüchtige Elemente neu zu ordnen, verlangt die Theorie, dass die Reihenfolge solcher Zugriffe Teil des beobachtbaren Verhaltens des Programms ist, dass der Compiler einen Code ausgibt, der die CPU daran hindert damit. Der Standard unterscheidet nicht zwischen dem, was der Compiler tut, und dem, was der Compiler durch den generierten Code macht.

Da niemand glaubt, dass der Compiler Befehle ausgeben muss, um die CPU daran zu hindern, Zugriffe auf flüchtige Variablen neu zu ordnen, und moderne Compiler dies nicht tun, sollte niemand glauben, dass der C++ - Standard es dem Compiler verbietet, Zugriffe auf bestimmte flüchtige Elemente neu zu ordnen .

+0

Meine Frage enthält nicht mehrere Threads, weshalb ich denke, dass der Vergleich von Verhalten, das von C++ 98 und C++ 11 angegeben wird, legitim ist. – KnowItAllWannabe

+0

@KnowItAllWannabe Dann ist Ihre Frage mehrdeutig. Wenn du nicht "in einer anderen Reihenfolge als in einem anderen Thread gesehen" meinst, was meinst du mit "neu geordnet"? –

+0

Angenommen, a und b sind speicherplatziert, mit einem Schreiben in den Steuerungsteil eines MMIO-Geräts und b Schreiben in den Datenteil. Nehmen Sie weiterhin an, dass die Hardware nicht korrekt funktioniert, wenn nicht etwas in den Steuerungsteil geschrieben wird, bevor irgendwelche Daten geschrieben werden. In diesem Fall ist es wichtig, dass a vor der Laufzeit geschrieben wird, wenn der Speicher vor dem Speicher im Quellcode liegt. – KnowItAllWannabe

0

Es hängt von Ihrem Compiler ab. Zum Beispiel garantiert MSVC++ ab Visual Studio 2005 * flüchtige Ressourcen werden nicht neu geordnet (tatsächlich, was Microsoft tat, gibt auf und nimmt an, dass Programmierer für immer missbrauchen werden volatile - MSVC++ fügt nun eine Speicherbarriere um bestimmte Verwendungen von volatile hinzu). Andere Versionen und andere Compiler verfügen möglicherweise nicht über solche Garantien.

Lange Rede kurzer Sinn: Wetten Sie nicht darauf. Entwerfen Sie Ihren Code richtig und missbrauchen Sie nicht flüchtig. Verwenden Sie stattdessen Speicherbarrieren oder vollständige Mutexe. C++ 11's atomic Arten helfen.

+1

Meine Frage ist, was der C++ 11 Standard spezifiziert, nicht was ein bestimmter Compiler tut. – KnowItAllWannabe

+0

Ich glaube, der richtige Weg ist zum Beispiel zu verwenden: 'volatile std :: atomic ', um sicherzustellen, Compiler wird nicht weg redundante Schreibvorgänge (das ist warum volatile) zu optimieren, und Neuordnungen zu vermeiden (das ist, warum std :: atomic). Für alle Interessierten gibt es einen verwandten Artikel 40 in Effective Modern C++. – marcinj

0

Für den Moment werde ich davon ausgehen, dass Ihre a=3 s sind nur ein Fehler beim Kopieren und Einfügen, und Sie wirklich gemeint, dass sie c=3 sein.

Die wirkliche Frage hier ist einer der Unterschiede zwischen der Auswertung und wie Dinge für einen anderen Prozessor sichtbar werden. Die Standards beschreiben die Reihenfolge der Auswertung.Von diesem Standpunkt aus sind Sie vollkommen richtig - bei Zuordnungen zu a, b und c in dieser Reihenfolge müssen die Zuordnungen in dieser Reihenfolge ausgewertet werden.

Das kann nicht entsprechen der Reihenfolge, in der diese Werte für andere Prozessoren obwohl sichtbar werden. Bei einer typischen (aktuellen) CPU schreibt diese Auswertung nur Werte in den Cache. Die Hardware kann die Dinge jedoch von dort neu ordnen, so dass (zum Beispiel) Schreibvorgänge in den Hauptspeicher in einer völlig anderen Reihenfolge erfolgen. Wenn ein anderer Prozessor versucht, die Werte zu verwenden, werden diese möglicherweise in einer anderen Reihenfolge angezeigt.

Ja, das ist völlig zulässig - die CPU bewertet die Zuordnungen immer noch genau in der vom Standard vorgegebenen Reihenfolge, damit die Anforderungen erfüllt sind. Der Standard stellt einfach keine Anforderungen an das, was nach der Auswertung passiert, was hier passiert.

Ich sollte hinzufügen: auf einigen Hardware ist es aber ausreichend. Zum Beispiel verwendet das x86 Cache-Snooping. Wenn ein anderer Prozessor versucht, einen Wert zu lesen, der von einem Prozessor aktualisiert wurde (aber immer noch nur im Cache ist), wird der Prozessor, der den aktuellen Wert hat, den Lesevorgang durch den anderen Prozessor halten Prozessor, bis der aktuelle Wert ausgeschrieben werden kann, damit der andere Prozessor den aktuellen Wert sieht.

Das ist jedoch bei der gesamten Hardware nicht der Fall. Während das strikte Modell die Dinge einfach hält, ist es auch ziemlich teuer, sowohl in Bezug auf zusätzliche Hardware, um Konsistenz zu gewährleisten, als auch in einfacher Geschwindigkeit, wenn/wenn Sie viele Prozessoren haben.

Edit: Wenn wir Threading für einen Moment ignorieren, wird die Frage ein wenig einfacher - aber nicht viel. Nach C++ 11, §1.9/12:

Wenn ein Anruf zu einer Bibliothek E/A-Funktion zurückgibt oder einen Zugang zu einem flüchtigen Objekt den Nebeneffekt bewertet wird als abgeschlossen betrachtet, auch wenn einige externe Aktionen durch den Aufruf impliziert (wie die I/O selbst) oder durch den flüchtigen Zugriff möglicherweise noch nicht abgeschlossen.

Als solche können die Zugriffe auf flüchtige Objekte müssen eingeleitet, um sein, aber nicht unbedingt abgeschlossen um. Leider ist es oft die Fertigstellung, die von außen sichtbar ist. Als solches kommen wir ziemlich auf die übliche as-if-Regel zurück: Der Compiler kann die Dinge so oft umgestalten, wie er will, solange es keine äußerlich sichtbare Veränderung erzeugt.

+0

Ich aktualisierte den Beitrag, um A/C-Ambiguität zu beseitigen, die in Chisnalls Artikel nicht vorhanden ist. Ich habe es auch aktualisiert, um anzuzeigen, dass dies keine Threading- oder Memory-Visibility-Threads-Frage ist. – KnowItAllWannabe

2

Sie haben Recht, er liegt falsch. Zugriffe auf bestimmte flüchtige Variablen können vom Compiler nicht neu geordnet werden, solange sie in getrennten vollständigen Ausdrücken auftreten, d. H. Durch C++ 98 als Sequenzpunkt getrennt sind, oder in C++ 11-Termen ein Zugriff vor dem anderen sequenziert ist.

Chisnall scheint zu erklären zu versuchen, warum volatile für das Schreiben von Thread-sicher Code nutzlos ist, durch eine einfache Implementierung Mutex zeigt auf volatile verlassen, die durch Compiler Umordnungen gebrochen werden würde. Er hat Recht, dass volatile für Thread-Sicherheit nutzlos ist, aber nicht für die Gründe, die er gibt. Es liegt nicht daran, dass der Compiler Zugriffe auf volatile Objekte neu anordnet, sondern weil die CPU sie neu anordnen könnte. Atomare Operationen und Speicherbarrieren verhindern, dass der Compiler und die Reihenfolge der Dinge über die Schranke hinweg neu anordnet, wie es für die Thread-Sicherheit erforderlich ist.

Siehe die untere rechte Zelle von Tabelle 1 bei Sutters informativem Artikel volatile vs volatile.

+0

Das ist Unsinn. Der C++ - Standard stellt keine Einschränkungen für das, was der Compiler tun kann, sondern nur, was der Code, den er erzeugt, einschränkt. Und wenn Sie denken, dass der C++ - Standard besagt, dass ein Compiler keinen Code erzeugen kann, der Schreibvorgänge an flüchtige Variablen umordnet, verstößt jeder x86-Compiler gegen den C++ - Standard, da sie die Schreibpuffer der CPU beim Schreiben auf flüchtige Variablen und diese nicht umgehen Puffer können Schreibvorgänge neu ordnen. –

+0

Du legst Worte in meinen Mund und sagst, dass es Unsinn ist. Es gibt einen großen Unterschied zwischen den Reorder-Zugriffen des Compilers und der Hardware, die es tut. Haben Sie den Teil meiner Antwort übersehen, bei dem ich gesagt habe, dass die CPU Zugriffe auf flüchtige Substanzen neu ordnen könnte? (Es wäre präziser gewesen, Hardware nicht CPU zu nennen, aber mein Punkt ist, dass die Hardware Dinge neu ordnen könnte, die der Compiler nicht beherrscht.) –

+0

Es gibt keinen Unterschied, aus der Sicht des C++ - Standards, zwischen dem Compiler, der etwas tut, und dem Compiler, der Anweisungen ausgibt, die die Hardware veranlassen, etwas zu tun. Wenn die CPU Zugriffe auf flüchtige Substanzen neu anordnet, erfordert der Standard nicht, dass ihre Reihenfolge beibehalten wird. Es kann nicht beides sein. –

Verwandte Themen