Ist die Erklärung der entspannten Reihenfolge in der Referenz falsch?

13

In der Dokumentation von std::memory_ordercppreference.com finden Sie ein Beispiel für eine entspannte Bestellung:

Entspannte Bestellung

Mit Tags versehene atomare Operationen memory_order_relaxedsind keine Synchronisationsoperationen. Sie legen keine Reihenfolge bei gleichzeitigen Speicherzugriffen fest. Sie garantieren nur die Konsistenz der Atomizität und der Änderungsreihenfolge.

Zum Beispiel, wenn x und y anfänglich Null sind,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

darf r1 == r2 == 42 erzeugen, denn obwohl A vor B in Thread 1 und C vor D in Thread 2 sequenziert wird , hindert nichts D daran, vor A in der Änderungsreihenfolge von y und B vor zu erscheinen erscheint vor C in der Änderungsreihenfolge von x. Die Nebenwirkung von D auf y könnte für die Last A in Gewinde 1 sichtbar sein, während die Nebenwirkung von B auf x für die Last C in Gewinde 2 sichtbar sein könnte. Dies kann insbesondere auftreten, wenn D vor C in abgeschlossen ist Thread 2, entweder aufgrund einer Neuordnung des Compilers oder zur Laufzeit.

es heißt "C wird vor D in Thread 2 sequenziert".

Gemäß der Definition von "vorher sequenziert", die in der Reihenfolge der Bewertung zu finden ist, wird die Bewertung von A abgeschlossen, bevor die Bewertung von B beginnt, wenn A vor B sequenziert wird. Da C vor D in Thread 2 sequenziert wird, muss C abgeschlossen sein, bevor D beginnt, daher wird der Bedingungsteil des letzten Satzes des Schnappschusses niemals erfüllt.

abigaile
quelle
Ist Ihre Frage speziell zu C ++ 11?
Neugieriger
nein, es gilt auch für c ++ 14,17. Ich weiß, dass sowohl der Compiler als auch die CPU C mit D neu anordnen können. Wenn die Neuordnung jedoch erfolgt, kann C nicht abgeschlossen werden, bevor D beginnt. Ich denke also, es gibt einen Terminologie-Missbrauch im Satz "A wird sequenziert - vor B in Thread 1 und C wird vor D in Thread 2 sequenziert". Es ist genauer zu sagen "Im Code wird A vor B in Thread 1 und C vor D in Thread 2 platziert". Das Ziel dieser Frage ist es, diesen Gedanken zu bestätigen
abigaile
Nichts wird als "Neuordnung" definiert.
Neugieriger

Antworten:

12

Ich glaube, dass die Präferenz richtig ist. Ich denke, das läuft auf die "Als-ob" -Regel [intro.execution] / 1 hinaus . Compiler sind nur verpflichtet, das beobachtbare Verhalten des in Ihrem Code beschriebenen Programms zu reproduzieren. Eine Sequenz-vor- Beziehung wird nur zwischen Bewertungen aus der Perspektive des Threads hergestellt, in dem diese Bewertungen durchgeführt werden [intro.execution] / 15 . Das heißt, wenn zwei nacheinander sequenzierte Auswertungen irgendwo in einem Thread erscheinen, muss sich der tatsächlich in diesem Thread ausgeführte Code so verhalten, als ob die erste Auswertung tatsächlich die zweite Auswertung beeinflusst. Zum Beispiel

int x = 0;
x = 42;
std::cout << x;

muss gedruckt werden 42. Der Compiler muss den Wert 42 jedoch nicht in einem Objekt speichern, xbevor er den Wert von diesem Objekt zurückliest, um ihn zu drucken. Es kann sich auch daran erinnern, dass der letzte Wert, in dem gespeichert xwurde, 42 war, und dann einfach den Wert 42 direkt ausdrucken, bevor eine tatsächliche Speicherung des Werts 42 an durchgeführt wird x. Wenn xes sich um eine lokale Variable handelt, kann sie auch einfach nachverfolgen, welcher Wert dieser Variablen zu einem beliebigen Zeitpunkt zuletzt zugewiesen wurde, und niemals ein Objekt erstellen oder den Wert 42 tatsächlich speichern. Der Thread kann den Unterschied nicht erkennen. Das Verhalten wird immer so sein, als ob es eine Variable gäbe und als ob der Wert 42 tatsächlich zuvor in einem Objekt gespeichert x worden wärevon diesem Objekt geladen werden. Dies bedeutet jedoch nicht, dass der generierte Maschinencode tatsächlich irgendwo irgendwo etwas speichern und laden muss. Alles, was erforderlich ist, ist, dass das beobachtbare Verhalten des generierten Maschinencodes nicht von dem zu unterscheiden ist, was das Verhalten wäre, wenn all diese Dinge tatsächlich passieren würden.

Wenn wir schauen

r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

dann ja, C wird vor D sequenziert. Aber wenn man diesen Thread isoliert betrachtet, beeinflusst nichts, was C tut, das Ergebnis von D. Und nichts, was D tut, würde das Ergebnis von C ändern. Der einzige Weg, wie einer den anderen beeinflussen könnte, wäre als indirekte Folge von etwas, das in einem anderen Thread passiert. Durch Angabe std::memory_order_relaxedhaben Sie jedoch ausdrücklich angegebendass die Reihenfolge, in der das Laden und Speichern von einem anderen Thread beobachtet wird, irrelevant ist. Da kein anderer Thread das Laden und Speichern in einer bestimmten Reihenfolge beobachten kann, kann kein anderer Thread dafür sorgen, dass C und D sich auf konsistente Weise gegenseitig beeinflussen. Somit ist die Reihenfolge, in der das Laden und Speichern tatsächlich ausgeführt wird, irrelevant. Somit kann der Compiler sie neu anordnen. Und wie in der Erklärung unter diesem Beispiel erwähnt, kann r1 == r2 == 42 tatsächlich zustande kommen, wenn der Speicher von D vor dem Laden von C ausgeführt wird…

Michael Kenzel
quelle
Im Wesentlichen besagt der Standard, dass C vor D geschehen muss , aber der Compiler glaubt, dass nicht bewiesen werden kann, ob C oder D als nächstes passiert ist, und ordnet sie aufgrund der Als-ob-Regel trotzdem neu an, richtig?
Fureeish
4
@Fureeish Nr. C muss vor D geschehen, soweit der Thread, auf dem sie auftreten, erkennen kann. Das Beobachten aus einem anderen Kontext kann mit dieser Ansicht nicht vereinbar sein.
Deduplikator
Es gibt keine "als ob Regel"
Neugieriger
4
@curiousguy Diese Behauptung scheint Ihren anderen früheren C ++ - Evangelisationen ähnlich zu sein .
Leichtigkeitsrennen im Orbit
1
@curiousguy Der Standard tut Etikett einen seiner Bestimmungen „die As-if - Regel“ in einer Fußnote: as-if „Regel‚‘Diese Bestimmung manchmal genannt wird“ intro.execution
Caleth
1

Es ist manchmal möglich, dass eine Aktion relativ zu zwei anderen Sequenzen von Aktionen angeordnet wird, ohne dass eine relative Reihenfolge der Aktionen in diesen Sequenzen relativ zueinander impliziert wird.

Angenommen, man hat zum Beispiel die folgenden drei Ereignisse:

  • Speichern Sie 1 bis p1
  • lade p2 in temp
  • Speichern Sie 2 bis p3

und das Lesen von p2 wird unabhängig nach dem Schreiben von p1 und vor dem Schreiben von p3 angeordnet, aber es gibt keine bestimmte Reihenfolge, an der sowohl p1 als auch p3 teilnehmen. Abhängig davon, was mit p2 gemacht wird, kann es für einen Compiler unpraktisch sein, p1 nach p3 zu verschieben und dennoch die erforderliche Semantik mit p2 zu erreichen. Angenommen, der Compiler wusste jedoch, dass der obige Code Teil einer größeren Sequenz war:

  • speichere 1 bis p2 [sequenziert vor dem Laden von p2]
  • [mach das oben]
  • speichere 3 in p1 [sequenziert nach dem anderen speichere in p1]

In diesem Fall könnte es bestimmen, dass es den Speicher nach dem obigen Code auf p1 umordnen und ihn mit dem folgenden Speicher konsolidieren könnte, was zu Code führt, der p3 schreibt, ohne zuerst p1 zu schreiben:

  • Stellen Sie die Temperatur auf 1 ein
  • Speichern Sie die Temperatur auf p2
  • Speichern Sie 2 bis p3
  • 3 bis p1 speichern

Obwohl es den Anschein haben mag, dass Datenabhängigkeiten dazu führen würden, dass sich bestimmte Teile der Sequenzierungsbeziehungen transitiv verhalten, kann ein Compiler Situationen identifizieren, in denen offensichtliche Datenabhängigkeiten nicht existieren, und würde daher nicht die erwarteten transitiven Effekte haben.

Superkatze
quelle
1

Wenn zwei Anweisungen vorhanden sind, generiert der Compiler den Code in sequentieller Reihenfolge, sodass der Code für die erste vor der zweiten platziert wird. CPU verfügt jedoch intern über Pipelines und kann Montagevorgänge parallel ausführen. Anweisung C ist eine Ladeanweisung. Während der Speicher abgerufen wird, verarbeitet die Pipeline die nächsten Anweisungen und da sie nicht von der Ladeanweisung abhängig sind, können sie ausgeführt werden, bevor C beendet ist (z. B. Daten für D befanden sich im Cache, C im Hauptspeicher).

Wenn der Benutzer die beiden Anweisungen wirklich benötigt, um nacheinander ausgeführt zu werden, können strengere Speicherreihenfolgeoperationen verwendet werden. Im Allgemeinen ist es den Benutzern egal, solange das Programm logisch korrekt ist.

edwinc
quelle
-7

Was auch immer Sie denken, ist gleichermaßen gültig. Der Standard sagt nicht, was sequentiell ausgeführt wird, was nicht und wie es verwechselt werden kann .

Es liegt an Ihnen und jedem einzelnen Programmierer, eine konsistente Semantik zu erstellen, die über dieses Durcheinander hinausgeht, eine Arbeit, die mehrere Doktorarbeiten verdient.

Neugieriger
quelle