Wird die Reihenfolge der Schreibvorgänge an separate Mitglieder einer flüchtigen Struktur garantiert beibehalten?

Lesezeit: 8 Minuten

Benutzer-Avatar
Ted Shaneyfelt

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?

  • 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 mit volatile. Wenn op diese Struktur für die IO-Interaktion verfügbar macht, dann verwenden volatile steht außer Frage. Das Tag von op deutet jedoch darauf hin, dass es sich in diesem Fall um Parallelität (Multithread-Programm) handelt std::atomic ist das richtige Werkzeug zu verwenden und nicht volatile. 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

Benutzer-Avatar
Nate Eldredge

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.

  • Vielen Dank für das Zitieren des Standards (da ich zufällig keine Kopie habe). Das scheint die Frage zu beantworten, aber Ihr Text “Sie weisen zwei flüchtige int-Objekte zu” ist insofern irreführend, als die Antwort nicht als dasselbe Objekt betrachtet würde anders sein, oder es müsste eine zusätzliche Einschränkung für den Compiler geben, um die Reihenfolge der flüchtigen Zugriffe beizubehalten, selbst wenn sie sich in nicht verwandten Objekten befinden. Vielleicht am besten das Zitat beibehalten und den Antworttext verfeinern …

    – Ted Shaneyfelt

    14. Dezember 2020 um 20:56 Uhr

  • Ich denke, dass das Ändern von Operationen auf gleichzeitig (mit einer Anweisung für zwei Aufgaben) als Neuordnung gelten sollte. Wenn nicht durch strenge Auslegung des Standards, dann sicherlich durch den Geist des Standards, gilt der Grund für eine solche Einschränkung (die eine Leistungseinbuße hat) unabhängig davon, ob Sie mit dem Wortlaut knifflig werden.

    – Ben

    14. Dezember 2020 um 21:04 Uhr

  • @Ben muss mit der gleichzeitigen Neuordnung richtig sein. Das Ändern von gleichzeitigen Operationen würde sich auf die Hardware auswirken, z. B. das Setzen von Datenbits und das anschließende Umschalten eines Strobe-Bits bei speicherabgebildeten E / A ist eindeutig etwas, das optimiert werden würde, wenn dies zulässig wäre.

    – Ted Shaneyfelt

    14. Dezember 2020 um 21:14 Uhr

  • Es ist implementierungsdefiniert, was den Zugriff auf ein volatile-qualifiziertes Objekt darstellt. Wenn die C-Implementierung auf Hardware abzielt, auf der die Auswirkungen eines 64-Bit-Schreibvorgangs die gleichen sein könnten wie zwei 32-Bit-Schreibvorgänge (z nicht unterscheidbar, also ist ein zwangsläufig gleichzeitiger 64-Bit-Schreibvorgang nicht von zwei 32-Bit-Schreibvorgängen zu unterscheiden, die tatsächlich gleichzeitig erfolgen), dann könnte es für die Implementierung sinnvoll sein, „Zugriff“ so zu definieren, dass ein 64-Bit-Schreibvorgang möglich ist Gebraucht.

    – Eric Postpischil

    14. Dezember 2020 um 21:14 Uhr


  • @TedShaneyfelt: Die Mitglieder eines Strukturtyps sind selbst Objekte. 6.2.5 (20): „Ein Strukturtyp beschreibt eine sequentiell zugewiesene, nicht leere Menge von Mitgliedern Objekte“. Wir führen also tatsächlich zwei Zugriffe auf flüchtige Objekte durch, und sie sind zufällig unterschiedliche Objekte, obwohl sie auch beide Teil des Objekts sind data. Ich habe den Wortlaut geändert, um klarzustellen, dass das Umordnen auch bei zwei Zugriffen auf dasselbe Objekt weiterhin verboten wäre (was hier nicht der Fall ist).

    – Nate Eldredge

    14. Dezember 2020 um 21:23 Uhr

Benutzer-Avatar
Suma

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):

  1. 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 .

  • “Dies wirft die Frage auf, ob volatile eine echte Bedeutung erhalten sollte, die sowohl Atomarität als auch Sichtbarkeit zwischen Threads bietet, ungefähr nach dem Vorbild von Java Volatiles. Obwohl wir glauben, dass dies abstrakt eine wesentliche Verbesserung darstellt, indem etwas, das derzeit vorhanden ist, Semantik erhält fast keine portable Semantik, es scheint eine Reihe von praktischen Hindernissen zu geben, die auf Abwärtskompatibilitätsprobleme zurückzuführen sind und uns zumindest zögern lassen.” – Hans Böhm & Nick Maclaren open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html

    – Ted Shaneyfelt

    15. Dezember 2020 um 19:04 Uhr

  • Die Besorgnis von Boehm & Maclaren hätte vielleicht dadurch ausgeräumt werden können, dass das Standardkomitee ein syntaktisches Konstrukt hinzugefügt hätte, innerhalb dessen Volatilität gezwungen wäre, sich mehr im Geiste der Volatilität zu verhalten, die sie aus Gründen der Abwärtskompatibilität zögern zu fordern. B. die neue Syntax: volatile { block } wäre eine ausreichende Ergänzung der Sprache, um Rückwärtskompatibilität zu ermöglichen, aber auch um ein intuitiveres, sinnvolleres und nützlicheres Verhalten flüchtiger Objekte innerhalb dieses Blocks zu ermöglichen. Wie beim Namensraum ist es möglicherweise am besten, ihm zu erlauben, sich über mehrere Funktionsdefinitionen zu erstrecken. So wie es ist, ist es doof.

    – Ted Shaneyfelt

    15. Dezember 2020 um 19:17 Uhr

  • Wenn Sie dies aus mehreren Threads verwenden, haben Sie ein Datenrennen und alle Wetten sind aus. nicht wie atomic Typen, volatile Objekte sind nicht threadsicher und vermeiden keine Datenrennen. Über die einzig sinnvolle Verwendung für volatile Heutzutage besteht darin, auf speicherabgebildete Hardwaregeräte zuzugreifen, und in diesem Fall wird der Speicher normalerweise auf eine maschinenspezifische Weise als “nicht zwischengespeichert” markiert, was die Neuordnung der CPU verhindern und sicherstellen soll, dass das Gerät Lasten und Speicherungen sieht (Baugruppenebene) Programmreihenfolge.

    – Nate Eldredge

    15. März um 22:25 Uhr

1384770cookie-checkWird die Reihenfolge der Schreibvorgänge an separate Mitglieder einer flüchtigen Struktur garantiert beibehalten?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy