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->ready
na 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.
źródło
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.Odpowiedzi:
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:
Przypis 1: ignorowanie słabo uporządkowanych sklepów NT; dlatego zwykle
sfence
robisz sklepy NT. Implementacje C11 / C ++ 11 zakładają, że nie korzystasz ze sklepów NT. Jeśli tak, użyj_mm_sfence
przed wydaniem, aby upewnić się, że szanuje twoje sklepy NT. (Zasadniczo nie używaj_mm_mfence
/_mm_sfence
w innych przypadkach ; zwykle musisz tylko zablokować zmianę kolejności kompilacji. Lub oczywiście po prostu użyj stdatomic.)źródło