Wie unterscheiden sich memory_order_seq_cst und memory_order_acq_rel?

Lesezeit: 9 Minuten

Wie unterscheiden sich memory order seq cst und memory order acq rel
Ein Programmierer

Stores sind Release-Operationen und Loads sind Acquiring-Operationen für beide. ich weiß das memory_order_seq_cst soll eine zusätzliche Gesamtordnung für alle Operationen auferlegen, aber ich versäume es, ein Beispiel zu erstellen, bei dem dies nicht der Fall ist, wenn alle memory_order_seq_cst werden ersetzt durch memory_order_acq_rel.

Übersehe ich etwas, oder ist der Unterschied nur ein Dokumentationseffekt, dh man sollte es nutzen memory_order_seq_cst wenn man beabsichtigen nicht mit einem entspannteren Modell zu spielen und zu verwenden memory_order_acq_rel beim Einschränken des entspannten Modells?

1642439291 31 Wie unterscheiden sich memory order seq cst und memory order acq rel
MSN

http://en.cppreference.com/w/cpp/atomic/memory_order hat ein gutes Beispiel ganz unten das geht nur mit memory_order_seq_cst. Im Wesentlichen memory_order_acq_rel stellt Lese- und Schreibreihenfolgen relativ zur atomaren Variable bereit, während memory_order_seq_cst bietet eine globale Lese- und Schreibreihenfolge. Das heißt, die sequentiell konsistenten Operationen sind in allen Threads in derselben Reihenfolge sichtbar.

Das Beispiel läuft darauf hinaus:

bool x= false;
bool y= false;
int z= 0;

a() { x= true; }
b() { y= true; }
c() { while (!x); if (y) z++; }
d() { while (!y); if (x) z++; }

// kick off a, b, c, d, join all threads
assert(z!=0);

Betrieb an z werden von zwei atomaren Variablen geschützt, nicht von einer, daher können Sie die Acquiring-Release-Semantik nicht verwenden, um dies zu erzwingen z wird immer erhöht.

  • @acidzombie24, selbst in diesem Fall, z wird 2.

    – MSN

    10. September 2012 um 20:40 Uhr

  • @CandyChiu Mit ack_rel, c() kann das wahrnehmen x=true; in a() passiert vorher y=true; in b() zur selben Zeit d() kann das wahrnehmen y=true; passiert vorher x=true; (aufgrund fehlender “globaler Ordnung”.) Insbesondere c() wahrnehmen kann x==true und y==false zur selben Zeit d() wahrnehmen kann y==true und x==false. So z kann durch keines der beiden erhöht werden c() oder d(). Mit seq_cst, wenn c() wahrnimmt x=true; passiert vorher y=true;, tut es auch d().

    – nodakai

    20. Januar 16 um 11:03 Uhr


  • @MSN Du meinst int z=0, nicht bool z=0

    – nodakai

    20. Januar 16 um 11:04 Uhr

  • @nodakai, Ihre Erklärung ist korrekt, aber ich denke, der Ausdruck “passiert vorher” kann irreführend sein, da der Kern des Problems mit der Erfassung und Freigabe darin besteht, dass keiner der beiden schreibt passiert-vorher das andere.

    – jhoffman0x

    1. Mai 18 um 3:50 Uhr


  • Dieses Beispiel verwendet reine Ladevorgänge und reine Speichervorgänge, keine tatsächlichen RMW-Operationen, die verwendet werden könnten std::memory_order_acq_rel. Bei einem atomaren Lesen-Ändern-Schreiben sind Laden und Speichern miteinander verbunden, da sie atomar sind. Ich bin mir nicht sicher, wann, wenn überhaupt acq_rel abweichen können seq_cst für sowas .fetch_add oder .compare_exchange_weak

    – Peter Cordes

    2. November 19 um 6:32 Uhr


1642439292 861 Wie unterscheiden sich memory order seq cst und memory order acq rel
Peter Kordes

Auf ISAs wie x86, wo Atomic Barriers zugeordnet ist und das eigentliche Maschinenmodell einen Speicherpuffer enthält:

  • seq_cst Stores erfordern das Leeren des Store-Puffers, sodass die späteren Lesevorgänge dieses Threads verzögert werden, bis der Store global sichtbar ist.

  • acquire oder release tun nicht müssen den Speicherpuffer leeren. Normale x86-Ladevorgänge und -Speicher haben im Wesentlichen acq- und rel-Semantik. (seq_cst plus einen Speicherpuffer mit Speicherweiterleitung.)

    Aber x86 atomare RMW-Operationen werden immer zu befördert seq_cst weil die x86 asm lock Präfix ist eine vollständige Speicherbarriere. Andere ISAs können das locker machen bzw acq_rel RMWs in Asm, wobei die Ladenseite in der Lage ist, begrenzte Nachbestellungen bei späteren Läden vorzunehmen. (Aber nicht auf eine Weise, die den RMW als nicht-atomar erscheinen lassen würde: Ist das atomare Lesen, Ändern und Schreiben für die Zwecke der Bestellung eine oder zwei Operationen?)


https://preshing.com/20120515/memory-reordering-in-the-act erwischt ist ein aufschlussreiches Beispiel für den Unterschied zwischen einem seq_cst-Speicher und einem einfachen Release-Speicher. (Es ist eigentlich mov + mfence vs. einfach mov in x86 asm. In der Praxis xchg ist eine effizientere Möglichkeit, einen seq_cst-Speicher auf den meisten x86-CPUs durchzuführen, aber GCC verwendet mov+mfence)


Fun Fact: Die LDAR-Acquire-Load-Anweisung von AArch64 ist eigentlich eine sequentiell-erwerben, mit einer besonderen Interaktion mit STLR. Erst mit ARMv8.3 LDAPR kann arm64 einfache Akquisitionsoperationen ausführen, die mit früheren Versionen und seq_cst-Speichern (STLR) neu geordnet werden können. (seq_cst Lasten verwenden immer noch LDAR, weil sie diese Interaktion mit STLR benötigen, um die sequentielle Konsistenz wiederherzustellen; seq_cst und release Stores verwenden beide STLR).

Mit STLR / LDAR erhalten Sie sequentielle Konsistenz, müssen aber nur den Speicherpuffer leeren vor dem nächsten LDAR, nicht sofort nach jedem seq_cst-Speicher vor anderen Operationen. Ich denke, echte AArch64-HW implementiert es auf diese Weise, anstatt einfach den Speicherpuffer zu leeren, bevor ein STLR übergeben wird.

Das Verstärken von rel oder acq_rel zu seq_cst durch die Verwendung von LDAR / STLR muss nicht teuer sein, es sei denn, Sie speichern etwas mit seq_cst und laden dann mit seq_cst etwas anderes. Dann ist es genauso schlimm wie x86.

Einige andere ISAs (wie PowerPC) haben eine größere Auswahl an Barrieren und können bis zu verstärken mo_rel oder mo_acq_rel billiger als mo_seq_cst, Aber ihre seq_cst kann nicht so billig sein wie AArch64; seq-cst-Stores benötigen eine vollständige Barriere.

AArch64 ist also eine Ausnahme von der Regel seq_cst Speicher entleeren den Speicherpuffer an Ort und Stelle, entweder mit einem speziellen Befehl oder einem Sperrbefehl danach. Es ist kein Zufall, dass ARMv8 entwickelt wurde nach C++11 / Java / etc. haben sich im Grunde darauf festgelegt, dass seq_cst der Standard für lockless atomare Operationen ist, daher war es wichtig, sie effizient zu gestalten. Und nachdem CPU-Architekten einige Jahre Zeit hatten, über Alternativen zum Bereitstellen von Barriereanweisungen nachzudenken oder einfach nur Anweisungen zum Laden/Speichern zu erhalten/freizugeben oder zu lockern.

  • Aber x86-Atomic-RMW-Operationen werden immer zu seq_cst hochgestuft, da das x86-asm-Sperrpräfix eine vollständige Speicherbarriere darstellt.“Was lässt Sie sagen, dass sie “befördert” werden? Auch die Exekutive könnte den Wert (normalerweise) spekulativ laden und die Berechnung durchführen, solange sie ihn später sicher neu lädt (gesperrtes Laden); wenn die Berechnung schnell ist, ist das wahrscheinlich uninteressant, aber immer noch (Ich nehme an, dass diese Dinge von Intel rein beschreibend für bestehende Designs und nicht für zukünftige Designs dokumentiert werden.)

    – Neugieriger

    28. November 19 um 1:01 Uhr

  • @curiousguy: die Full-Memory-Barrier-Natur des x86 lock Präfix wird von Intel und AMD in ihren x86 ISA-Handbüchern sorgfältig dokumentiert. (Verhält sich lock xchg genauso wie mfence?). Es ist definitiv für zukünftige x86-CPUs garantiert; wie sonst könnten Compiler asm sicher und zukunftssicher machen? Das meine ich damit, dass Compiler alle RMW-Operationen auf seq_cst im asm verstärken müssen, wodurch der Speicherpuffer geleert wird, bevor das RMW seine Sache macht.

    – Peter Cordes

    28. November 19 um 3:03 Uhr


  • Was wird genau garantiert? Dass die CPU nicht versuchen wird, den bereits geladenen Wert und die Berechnung im Voraus in den Speicher zu bringen, also ein kostspieliges RMW zu beschleunigen, sagt xdiv (oder xcos wenn die FPU beschließt, RMW zu unterstützen)?

    – Neugieriger

    30. November 19 um 4:53 Uhr

  • @curiousguy: Aber wie auch immer, wenn eine hypothetische Implementierung versuchen wollte, früh zu laden, um einen billigeren atomaren Austausch einzurichten, um den RMW tatsächlich zu implementieren, konnte sie nur das tun spekulativ und rollen Sie bei Fehlspekulationen zurück (wenn sich die Linie geändert hat, bevor das Laden architektonisch zulässig war). Regelmäßige Ladevorgänge funktionieren bereits auf diese Weise, um Leistung zu erzielen und gleichzeitig eine starke Ladereihenfolge beizubehalten. (Siehe die machine_clears.memory_ordering Leistungsindikator: Warum die Pipeline für eine Verletzung der Speicherreihenfolge leeren, die von anderen logischen Prozessoren verursacht wird?)

    – Peter Cordes

    30. November 19 um 5:18 Uhr


  • @PeterCordes – Ich denke nicht einmal, dass es hypothetisch ist: Ich denke, so werden atomare Operationen (manchmal) auf dem aktuellen Intel x86 implementiert. Das heißt, dass sie die Cache-Zeile in einem optimistisch gesperrten Zustand laden, das „Front-End“ des RMW (einschließlich der ALU-Operation) ausführen und dann im „Back-End“ des RMW überprüfen, ob bei der Ausführung alles in Ordnung war -at-Retire-Operation, die die gesamte Bestellung sicherstellt. Dies funktioniert hervorragend, wenn der Standort nicht umstritten ist. Wenn dies oft fehlschlägt, wechselt ein Prädiktor den Modus, um das Ganze im Ruhestand zu tun, was zu einer größeren Blase in der Pipeline führt (daher “manchmal”).

    – BeeOnRope

    30. November 19 um 18:16 Uhr


Versuchen Sie, einen Dekkers- oder Petersons-Algorithmus nur mit Acquiring/Release-Semantik zu erstellen.

Das wird nicht funktionieren, da die Acquir/Release-Semantik dies nicht bietet [StoreLoad] Zaun.

Im Falle des Dekkers-Algorithmus:

flag[self]=1 <-- STORE
while(true){
    if(flag[other]==0) { <--- LOAD
        break;
    }
    flag[self]=0;
    while(turn==other);
    flag[self]=1        
}

Ohne [StoreLoad] Zaun könnte der Laden vor die Last springen und dann würde der Algorithmus brechen. 2 Threads gleichzeitig würden sehen, dass die andere Sperre frei ist, ihre eigene Sperre setzen und fortfahren. Und jetzt haben Sie 2 Threads im kritischen Abschnitt.

Verwenden Sie weiterhin die Definition und das Beispiel von Speicher_Reihenfolge. Ersetzen Sie jedoch memory_order_seq_cst durch memory_order_release in store und memory_order_acquire in load.

Release-Acquire-Bestellung garantiert alles, was vor a passiert ist Geschäft in einem Thread wird ein sichtbarer Nebeneffekt in dem Thread, der geladen hat. Aber in unserem Beispiel passiert vorher nichts Geschäft sowohl in thread0 als auch in thread1.

x.store(true, std::memory_order_release); // thread0

y.store(true, std::memory_order_release); // thread1

Darüber hinaus ist ohne memory_order_seq_cst die sequentielle Reihenfolge von Thread2 und Thread3 nicht garantiert. Sie können sich vorstellen, dass sie werden:

if (y.load(std::memory_order_acquire)) { ++z; } // thread2, load y first
while (!x.load(std::memory_order_acquire)); // and then, load x

if (x.load(std::memory_order_acquire)) { ++z; } // thread3, load x first
while (!y.load(std::memory_order_acquire)); // and then, load y

Wenn also Thread2 und Thread3 vor Thread0 und Thread1 ausgeführt werden, bedeutet das, dass sowohl x als auch y falsch bleiben, also wird ++z nie berührt, z bleibt 0 und die Bestätigung wird ausgelöst.

Wenn jedoch memory_order_seq_cst ins Spiel kommt, richtet es eine einzige Gesamtmodifikationsreihenfolge aller so gekennzeichneten atomaren Operationen ein. Also in thread2, x.load dann y.load; in Thread3 sind y.load und dann x.load sichere Dinge.

.

522230cookie-checkWie unterscheiden sich memory_order_seq_cst und memory_order_acq_rel?

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

Privacy policy