C11 Atomic Acquire / Release und x86_64 mangelnde Lade- / Speicherkohärenz?

10

Ich habe Probleme mit Abschnitt 5.1.2.4 des C11-Standards, insbesondere mit der Semantik von Release / Acquire. Ich stelle fest, dass https://preshing.com/20120913/acquire-and-release-semantics/ (unter anderem) besagt, dass:

... Die Release-Semantik verhindert, dass die Schreibfreigabe mit einer Lese- oder Schreiboperation, die ihr in der Programmreihenfolge vorausgeht, im Speicher neu angeordnet wird.

Also für Folgendes:

typedef struct test_struct
{
  _Atomic(bool) ready ;
  int  v1 ;
  int  v2 ;
} test_struct_t ;

extern void
test_init(test_struct_t* ts, int v1, int v2)
{
  ts->v1 = v1 ;
  ts->v2 = v2 ;
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}

extern int
test_thread_1(test_struct_t* ts, int v2)
{
  int v1 ;
  while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v2 = v2 ;       // expect read to happen before store/release 
  v1     = ts->v1 ;   // expect write to happen before store/release 
  atomic_store_explicit(&ts->ready, true, memory_order_release) ;
  return v1 ;
}

extern int
test_thread_2(test_struct_t* ts, int v1)
{
  int v2 ;
  while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
  ts->v1 = v1 ;
  v2     = ts->v2 ;   // expect write to happen after store/release in thread "1"
  atomic_store_explicit(&ts->ready, false, memory_order_release) ;
  return v2 ;
}

wo diese ausgeführt werden:

>   in the "main" thread:  test_struct_t ts ;
>                          test_init(&ts, 1, 2) ;
>                          start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
>                          start thread "1" which does: r1 = test_thread_1(&ts, 4) ;

Ich würde daher erwarten, dass Thread "1" r1 == 1 und Thread "2" r2 = 4 hat.

Ich würde das erwarten, weil (gemäß den Absätzen 16 und 18 von Abschnitt 5.1.2.4):

  • Alle (nicht atomaren) Lese- und Schreibvorgänge werden "vor" sequenziert und daher "vor" dem atomaren Schreiben / Freigeben in Thread "1" durchgeführt.
  • welches "Inter-Thread-passiert-vor" dem atomaren Lesen / Erfassen in Thread "2" (wenn es "wahr" lautet),
  • was wiederum "vorher sequenziert" ist und daher "vor" dem (nicht atomaren) Lesen und Schreiben (im Thread "2") geschieht.

Es ist jedoch durchaus möglich, dass ich den Standard nicht verstanden habe.

Ich stelle fest, dass der für x86_64 generierte Code Folgendes enthält:

test_thread_1:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  jne    <test_thread_1>  -- while is true
  mov    %esi,0x8(%rdi)   -- (W1) ts->v2 = v2
  mov    0x4(%rdi),%eax   -- (R1) v1     = ts->v1
  movb   $0x1,(%rdi)      -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
  retq   

test_thread_2:
  movzbl (%rdi),%eax      -- atomic_load_explicit(&ts->ready, memory_order_acquire)
  test   $0x1,%al
  je     <test_thread_2>  -- while is false
  mov    %esi,0x4(%rdi)   -- (W2) ts->v1 = v1
  mov    0x8(%rdi),%eax   -- (R2) v2     = ts->v2   
  movb   $0x0,(%rdi)      -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
  retq   

Und vorausgesetzt, dass R1 und X1 in dieser Reihenfolge auftreten, ergibt dies das erwartete Ergebnis.

Mein Verständnis von x86_64 ist jedoch, dass Lesevorgänge in der Reihenfolge mit anderen Lese- und Schreibvorgängen in der Reihenfolge mit anderen Schreibvorgängen erfolgen, Lese- und Schreibvorgänge jedoch möglicherweise nicht in der richtigen Reihenfolge. Was bedeutet, dass X1 vor R1 und sogar X1, X2, W2, R1 in dieser Reihenfolge auftreten kann - glaube ich. [Dies scheint äußerst unwahrscheinlich, aber wenn R1 durch einige Cache-Probleme aufgehalten würde?]

Bitte: Was verstehe ich nicht?

Ich stelle fest, dass, wenn ich die Ladevorgänge / Speicher von ts->readyin ändere memory_order_seq_cst, der für die Speicher generierte Code lautet:

  xchg   %cl,(%rdi)

Das stimmt mit meinem Verständnis von x86_64 überein und wird das erwartete Ergebnis liefern.

Chris Hall
quelle
5
Unter x86 verfügen alle normalen (nicht temporären) Speicher über eine Release-Semantik. Intel® 64- und IA-32-Architekturen Software-Entwicklerhandbuch Band 3 (3A, 3B, 3C und 3D): Systemprogrammierhandbuch , 8.2.3.3 Stores Are Not Reordered With Earlier Loads. Ihr Compiler übersetzt Ihren Code also korrekt (wie überraschend), sodass Ihr Code effektiv vollständig sequentiell ist und nichts Interessantes gleichzeitig passiert.
EOF
Vielen Dank ! (Ich war leise verrückt.) FWIW Ich empfehle Link - insbesondere Abschnitt 3, das "Programmierermodell". Um den Fehler zu vermeiden, in den ich geraten bin, beachten Sie, dass es in "3.1 The Abstract Machine" "Hardware-Threads" gibt, von denen jeder "ein einzelner Strom der Befehlsausführung in der Reihenfolge " ist (meine Betonung wurde hinzugefügt). Ich kann jetzt wieder versuchen, den C11-Standard zu verstehen ... mit weniger kognitiver Dissonanz :-)
Chris Hall

Antworten:

1

Das Speichermodell von x86 besteht im Wesentlichen aus sequentieller Konsistenz plus einem Speicherpuffer (mit Speicherweiterleitung). Jeder Store ist also ein Release-Store 1 . Aus diesem Grund benötigen nur seq-cst-Geschäfte spezielle Anweisungen. ( C / C ++ 11-Atomics-Zuordnungen zu asm ). Außerdem enthält https://stackoverflow.com/tags/x86/info einige Links zu x86-Dokumenten, einschließlich einer formalen Beschreibung des x86-TSO-Speichermodells (für die meisten Menschen grundsätzlich nicht lesbar; erfordert das Durchblättern vieler Definitionen).

Da Sie bereits Jeff Preshings ausgezeichnete Artikelserie lesen, verweise ich Sie auf eine andere, die ausführlicher behandelt wird: https://preshing.com/20120930/weak-vs-strong-memory-models/

Die einzige Neuordnung, die auf x86 zulässig ist, ist StoreLoad, nicht LoadStore , wenn wir in diesen Begriffen sprechen. (Die Speicherweiterleitung kann besonders unterhaltsam sein, wenn ein Ladevorgang einen Speicher nur teilweise überlappt. Global Invisible-Anweisungen zum Laden , obwohl dies im vom Compiler generierten Code für nie angezeigt wird stdatomic.)

@EOF kommentierte mit dem richtigen Zitat aus Intels Handbuch:

Intel® 64- und IA-32-Architekturen Software-Entwicklerhandbuch Band 3 (3A, 3B, 3C und 3D): Systemprogrammierungshandbuch, 8.2.3.3 Speicher werden bei früheren Ladevorgängen nicht neu angeordnet.


Fußnote 1: Ignorieren schwach geordneter NT-Geschäfte; Dies ist der Grund, warum Sie normalerweise sfencenach dem Ausführen von NT-Speichern. Bei C11 / C ++ 11-Implementierungen wird davon ausgegangen, dass Sie keine NT-Speicher verwenden. Wenn dies der Fall ist, verwenden Sie _mm_sfencevor einem Freigabevorgang, um sicherzustellen, dass Ihre NT-Speicher respektiert werden. ( Verwenden Sie _mm_mfence/ _mm_sfencein anderen Fällen im Allgemeinen nicht / normalerweise müssen Sie nur die Neuordnung zur Kompilierungszeit blockieren. Oder verwenden Sie einfach stdatomic.)

Peter Cordes
quelle
Ich finde das x86-TSO: Ein rigoroses und verwendbares Programmierermodell für x86-Multiprozessoren besser lesbar als die (verwandte) formale Beschreibung, auf die Sie verwiesen haben. Mein eigentliches Ziel ist es jedoch, die Abschnitte 5.1.2.4 und 7.17.3 des C11 / C18-Standards vollständig zu verstehen. Insbesondere denke ich, dass ich Release / Acquire / Acquire + Release bekomme, aber memory_order_seq_cst wird separat definiert und ich habe Mühe zu sehen, wie sie alle zusammenpassen :-(
Chris Hall
@ChrisHall: Ich fand es hilfreich zu erkennen, wie schwach acq / rel sein kann, und dafür muss man sich Maschinen wie POWER ansehen, die IRIW-Nachbestellungen durchführen können. (was seq-cst verbietet, acq / rel aber nicht). Werden zwei atomare Schreibvorgänge an verschiedenen Stellen in verschiedenen Threads von anderen Threads immer in derselben Reihenfolge angezeigt? . Auch wie erreicht man eine StoreLoad-Barriere in C ++ 11? Es gibt einige Diskussionen darüber, wie wenig der Standard formal für die Bestellung außerhalb von Fällen mit Synchronisierung mit oder mit allen Folgen garantiert.
Peter Cordes
@ChrisHall: Die Hauptsache, die seq-cst macht, ist die Blockierung der StoreLoad-Neuordnung. (Auf x86 ist dies das einzige, was es über acq / rel hinaus tut). preshing.com/20120515/memory-reordering-caught-in-the-act verwendet asm, aber es entspricht seq-cst vs. acq / rel
Peter Cordes