Implizieren std::mutex und std::atomic einen full fence (full barrier) ?



  • Hallo,

    nach H. Sutter implizieren std::mutex keinen full fence, sondern Instruktionen können in die kritische Section hineingezogen werden. Der Standard sagt hierzu leider nichts.

    Wie sieht es da mit atomics aus? Darf man das so verstehen dass ein *.load auf einem atomic ein acquire darstellt und ein *.store ein release? Doch wie sieht es hier bezüglich reordering aus? Danke für detaillierte Aufklärung.

    Danke und Gruß
    Zambo


  • Mod

    See [intro.races]. unlock ing a mutex synchronizes with it being lock ed. This implies that the unlock inter-thread happens before any operation after the lock as per [intro.races]/(9.3.1), and hence by (9.3.2) it follows that any operation before the mutex unlock happens before any operation following the lock. AFAIK that's how a full fence is defined, and as you can see, the standard is pretty clear (and refreshingly formal) about this matter.

    Concerning atomics, [intro.races]/6 mentions

    For example, an atomic store-release synchronizes with a load-acquire that takes its value from the store ([atomics.order]



  • Erstmal generell...
    Das Locken einer Mutex hat üblicherweise acquire Semantik.
    Das Freigeben hat üblicherweise release Semantik.
    Also kein full-fence.
    Das sind die üblichen Regeln, weil das die Regeln sind die quasi für jede vernünftige Verwendung einer Mutex ausreichen und stärkere Garantien auf vielen Plattformen teurer wären.

    Und daher würde es mich wundern, wenn der C++ Standard etwas anderes vorschreiben würde.

    Mat12345 schrieb:

    Wie sieht es da mit atomics aus? Darf man das so verstehen dass ein *.load auf einem atomic ein acquire darstellt und ein *.store ein release? Doch wie sieht es hier bezüglich reordering aus?

    Sämtliche Operationen auf std::atomic haben nen memory order Parameter, der nen Default-Wert von memory_order_seq_cst hat. Also kompletter, totaler full-fence.
    Wenn du sicher bist dass du bloss load(memory_order_acquire) und store(memory_order_release) brauchst, wärst du gut beraten das explizit anzugeben, weil sie Sache dadurch schneller wird. Auf x86 sogar um Grössenordnungen schneller. Also so grob 10-20x schneller um genau zu sein.

    Bei sämtlichen read-modify-write Operationen ist der Unterschied (deutlich) kleiner bzw. je nach Plattform gibt es keinen Unterschied (z.B. bei x86 ist read-modify-write AFAIK immer seq_cst).

    @Arcoth

    Arcoth schrieb:

    This implies that the unlock inter-thread happens before any operation after the lock as per [intro.races]/(9.3.1), and hence by (9.3.2) it follows that any operation before the mutex unlock happens before any operation following the lock. AFAIK that's how a full fence is defined, and as you can see, the standard is pretty clear (and refreshingly formal) about this matter.

    Also... ich würde mal sagen nö. Lies mal etwas weiter dazu... z.B.
    http://eel.is/c++draft/intro.multithread#def:synchronize_with

    Certain library calls synchronize with other library calls performed by another thread.
    For example, an atomic store-release synchronizes with a load-acquire that takes its value from the store ([atomics.order]).
    [ Note: Except in the specified cases, reading a later value does not necessarily ensure visibility as described below.
    Such a requirement would sometimes interfere with efficient implementation.
    — end note]
    [ Note: The specifications of the synchronization operations define when one reads the value written by another.
    For atomic objects, the definition is clear.
    All operations on a given mutex occur in a single total order.
    Each mutex acquisition “reads the value written” by the last mutex release.
    — end note]

    Das klingt nicht gerade nach einem "full fence". Denn ein "full fence" ist eben gerade das: "full". Also memory_order_seq_cst.


  • Mod

    Ich hab wohl missverstanden, was eine full fence ist; was nicht garantiert ist, ist dass Instruktionen nach dem unlock nach Instruktionen vor dem lock geschehen. Das ist auch nicht notwendig, und würde, wie du sagst, in vielen Fällen überflüssige Performancedefizite mit sich bringen.



  • Naja, es geht um die Loads und Stores.

    Und ein Full Fence erlaubt keinerlei Reordering von Loads oder Stores über den Fence hinweg. D.h. er muss z.B. auch warten bis alle in Store-Buffer gepufferten Stores ihren Weg in den Cache gemacht haben. Ob ein Full Fence auf den üblichen Architekturen auch das Reordering von anderen Befehlen (die z.B. nur Register verwenden) verhindert weiss ich nicht. So wie ich die Sache verstehe müsste er das aber nichtmal.


  • Mod

    hustbaer schrieb:

    Und ein Full Fence erlaubt keinerlei Reordering von Loads oder Stores über den Fence hinweg. D.h. er muss z.B. auch warten bis alle in Store-Buffer gepufferten Stores ihren Weg in den Cache gemacht haben.

    Wie meinst du das? Stores landen doch sofort im L1 Cache. Meinst du, dass der write buffer vom L1 Cache geleert werden soll? Oder dass Cache Kohärenz erst greifen muss, damit der Cache von anderen Kernen diesen Wert speichert?



  • Nönö, x86 hat Store-Buffer vor dem L1 Cache (und vermutlich andere Architekturen ebenso). Wenn ich mich richtig erinnere 2 Stück pro Core. Stores landen erstmal in einem Store-Buffer, und werden von dort aus dann so schnell wie möglich in den L1 Cache geschrieben. Die Reihenfolge der Stores wird dabei nicht verändert, aber es kann dadurch die Sichtbarkeit von Stores hinter Loads verschoben werden.

    Natürlich wird diese Trickserei nicht auf dem eigenen Core sichtbar. Wenn ein Load unmittelbar auf einen Store folgt, der eine vom Load betroffene Adresse ändert, dann sorgt die CPU natürlich dafür dass die Werte aus dem Store-Buffer berücksichtigt werden.

    Aus Sicht einer anderen CPU sieht es dadurch aber so aus als ob die CPU Stores hinter Loads verschieben würde. (Was sie genaugenommen ja auch macht, aber halt nicht durch klassisches Reordering.)

    Das klassische Beispiel um den Effekt zu demonstrieren ist (a und b initial 0):
    Thread 1:

    a = 1
    if (b == 0)
        print "Thread 1 was first."
    

    Thread 2:

    b = 1
    if (a == 0)
        print "Thread 2 was first."
    

    Ganz ohne Reordering von Loads und Stores muss das funktionieren, und es darf maximal ein Thread glauben dass er der erste war. Durch die Store-Buffer kann es aber passieren dass beide glauben dass sie der erste waren.

    Für relaxed/acquire/release ist das OK. Für seq_cst natürlich nicht mehr. Und daher muss auch für normale Stores auf x86 bei seq_cst ne fette Barrier her, die dann auch entsprechend teuer ist.


  • Mod

    Interessant. Vielen Dank für die ausführliche Erklärung! 👍



  • Gerne. Danke auch an rapso, der hat's nämlich mir vor gar nicht langer Zeit erklärt 🙂


Log in to reply