C11 Atomic Acquire / Release i x86_64 brak spójności ładowania / przechowywania?

10

Walczę z sekcją 5.1.2.4 normy C11, w szczególności semantyką wydania / nabycia. Zauważam, że https://preshing.com/20120913/acquire-and-release-semantics/ (między innymi) stwierdza, że:

... Semantyka wydania zapobiega zmianie kolejności pamięci wydania-zapisu przy każdej operacji odczytu lub zapisu, która poprzedza ją w kolejności programów.

Tak więc dla następujących:

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 ;
}

gdzie są wykonywane:

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

Oczekiwałbym zatem, że wątek „1” ma r1 == 1, a wątek „2” ma r2 = 4.

Spodziewałbym się tego, ponieważ (zgodnie z ust. 16 i 18 sekcji 5.1.2.4):

  • wszystkie (nieatomowe) odczyty i zapisy są „sekwencjonowane przed”, a zatem „zdarzają się przed” zapisem / zwolnieniem atomowym w wątku „1”,
  • który „między-wątkiem-dzieje-przed” atomowy odczyt / nabycie w wątku „2” (gdy czyta „prawda”),
  • który z kolei jest „sekwencjonowany przed”, a zatem „dzieje się przed” (nie atomowy) odczytuje i zapisuje (w wątku „2”).

Jest jednak całkiem możliwe, że nie zrozumiałem normy.

Zauważam, że kod wygenerowany dla x86_64 obejmuje:

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   

I pod warunkiem, że R1 i X1 występują w tej kolejności, daje to oczekiwany przeze mnie wynik.

Ale rozumiem x86_64, że odczyty odbywają się w kolejności z innymi odczytami, a zapisy są wykonywane w kolejności z innymi zapisami, ale odczyty i zapisy mogą nie zachodzić w kolejności względem siebie. Co oznacza, że ​​X1 może się zdarzyć przed R1, a nawet X1, X2, W2, R1 w takiej kolejności - wierzę. [Wydaje się to niezwykle mało prawdopodobne, ale jeśli R1 byłyby utrzymywane przez pewne problemy z pamięcią podręczną?]

Proszę: czego nie rozumiem?

Zwracam uwagę, że jeśli zmienię obciążenia / sklepy ts->readyna memory_order_seq_cst, kod wygenerowany dla sklepów to:

  xchg   %cl,(%rdi)

co jest zgodne z moim rozumieniem x86_64 i da oczekiwany rezultat.

Chris Hall
źródło
5
Na x86 wszystkie zwykłe (nie-czasowe) sklepy mają semantykę wydania. Intel® 64 oraz IA-32 architektury oprogramowania Program Manual Volume 3 (3A, 3B, 3C i 3D): System Programming Guide , 8.2.3.3 Stores Are Not Reordered With Earlier Loads. Więc twój kompilator poprawnie tłumaczy twój kod (jakie to zaskakujące), tak że twój kod jest faktycznie całkowicie sekwencyjny i nic ciekawego się nie dzieje jednocześnie.
EOF
Dziękuję Ci ! (Ja jechałem spokojnie Bonkers.) FWIW Polecam odnośnik - zwłaszcza rozdział 3, „Programisty Model”. Ale aby uniknąć błędu, w który wpadłem, zauważ, że w „3.1 The Abstract Machine” są „wątki sprzętowe”, z których każdy jest „pojedynczym strumieniem wykonywania instrukcji w kolejności ” (moje podkreślenie dodane). Teraz mogę wrócić do próby zrozumienia standardu C11 ... z mniejszym dysonansem poznawczym :-)
Chris Hall,

Odpowiedzi:

1

Model pamięci x86 ma w zasadzie spójność sekwencyjną plus bufor pamięci (z przekazywaniem pamięci). Więc każdy sklep jest sklepem z wydaniami 1 . Dlatego tylko sklepy Seq-cst potrzebują specjalnych instrukcji. ( Odwzorowania atomowe C / C ++ 11 na asm ). Ponadto https://stackoverflow.com/tags/x86/info zawiera kilka linków do dokumentów x86, w tym formalny opis modelu pamięci x86-TSO (w zasadzie nieczytelny dla większości ludzi; wymaga przebrnięcia przez wiele definicji).

Ponieważ czytasz już znakomitą serię artykułów Jeffa Preshinga, wskażę Ci kolejny, który zawiera bardziej szczegółowe informacje: https://preshing.com/20120930/weak-vs-strong-memory-models/

Jedyne zmiany kolejności, które są dozwolone na x86, to StoreLoad, a nie LoadStore , jeśli mówimy w tych kategoriach. (Przekazywanie do sklepu może robić dodatkowe fajne rzeczy, jeśli ładunek tylko częściowo pokrywa się ze sklepem; globalnie niewidoczne instrukcje ładowania , chociaż nigdy nie otrzymasz tego w kodzie generowanym przez kompilator stdatomic).

@EOF skomentował właściwy cytat z podręcznika Intela:

Podręcznik programisty dla architektur Intel® 64 i IA-32 Tom 3 (3A, 3B, 3C i 3D): Podręcznik programowania systemu, 8.2.3.3 Sklepy nie są porządkowane przy wcześniejszych obciążeniach.


Przypis 1: ignorowanie słabo uporządkowanych sklepów NT; dlatego zwykle sfencerobisz sklepy NT. Implementacje C11 / C ++ 11 zakładają, że nie korzystasz ze sklepów NT. Jeśli tak, użyj _mm_sfenceprzed wydaniem, aby upewnić się, że szanuje twoje sklepy NT. (Zasadniczo nie używaj _mm_mfence/ _mm_sfencew innych przypadkach ; zwykle musisz tylko zablokować zmianę kolejności kompilacji. Lub oczywiście po prostu użyj stdatomic.)

Peter Cordes
źródło
Uważam, że x86-TSO: Rygorystyczny i użyteczny model programisty dla wieloprocesorów x86 jest bardziej czytelny niż (powiązany) opis formalny, o którym wspominałeś. Ale moją prawdziwą ambicją jest pełne zrozumienie sekcji 5.1.2.4 i 7.17.3 normy C11 / C18. W szczególności wydaje mi się, że dostaję Release / Acquire / Acquire + Release, ale memory_order_seq_cst jest zdefiniowane osobno i staram się zobaczyć, jak wszystkie one pasują do siebie :-(
Chris Hall
@ChrisHall: Odkryłem, że pomogło mi uświadomić sobie dokładnie, jak słaby może być acq / rel, i do tego trzeba przyjrzeć się maszynom takim jak POWER, które mogą zmieniać kolejność IRIW. (który nie zezwala na sekwencję-cst, ale acq / rel nie). Czy dwa zapisy atomowe w różnych lokalizacjach w różnych wątkach zawsze będą widziane w tej samej kolejności przez inne wątki? . Jak również osiągnąć barierę StoreLoad w C ++ 11? ma pewne dyskusje na temat tego, jak niewiele standard formalnie gwarantuje przy zamawianiu poza przypadkami sychronizującymi lub wszystkimi innymi sekwencjami.
Peter Cordes
@ChrisHall: Najważniejsze, co robi seq-cst, to blokowanie zmiany kolejności StoreLoad. (Na x86 to jedyne, co robi poza acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act używa asm, ale jest to odpowiednik seq-cst vs. acq / rel
Peter Cordes