C11 Atomic Acquire / Release e mancanza di carico / coerenza x86_64?


10

Sto lottando con la Sezione 5.1.2.4 della norma C11, in particolare la semantica di Release / Acquire. Lo noto https://preshing.com/20120913/acquire-and-release-semantics/ (tra gli altri) afferma che:

... La semantica di rilascio impedisce il riordino della memoria del write-release con qualsiasi operazione di lettura o scrittura che la precede nell'ordine del programma.

Quindi, per quanto segue:

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

dove vengono eseguiti:

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

Mi aspetto quindi che il thread "1" abbia r1 == 1 e il thread "2" abbia r2 = 4.

Me lo aspetterei perché (in seguito ai paragrafi 16 e 18 della sezione 5.1.2.4):

  • tutte le letture e le scritture (non atomiche) sono "sequenziate prima" e quindi "avvengono prima" della scrittura / versione atomica nel thread "1",
  • quale "inter-thread-succede-prima" l'atomico legge / acquisisce nel thread "2" (quando legge "vero"),
  • che a sua volta è "sequenziato prima" e quindi "accade prima" il (non atomico) legge e scrive (nel thread "2").

Tuttavia, è del tutto possibile che non ho capito lo standard.

Osservo che il codice generato per x86_64 include:

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   

E a condizione che R1 e X1 avvengano in quell'ordine, questo dà il risultato che mi aspetto.

Ma la mia comprensione di x86_64 è che le letture avvengono in ordine con altre letture e scritture avvengono in ordine con altre scritture, ma le letture e le scritture potrebbero non avvenire in ordine tra loro. Ciò implica che è possibile che X1 accada prima di R1, e anche che X1, X2, W2, R1 avvengano in quell'ordine - credo. [Questo sembra disperatamente improbabile, ma se R1 fosse trattenuto da alcuni problemi di cache?]

Per favore: cosa non capisco?

Noto che se cambio i carichi / negozi di ts->readya memory_order_seq_cst, il codice generato per i negozi è:

  xchg   %cl,(%rdi)

che è coerente con la mia comprensione di x86_64 e darà il risultato che mi aspetto.


5
Su x86, tutti i negozi ordinari (non temporali) hanno una semantica di rilascio. Intel® 64 e IA-32 Architetture Software Developer Manuale Volume 3 (3A, 3B, 3C e 3D): sistema di programmazione Guida , 8.2.3.3 Stores Are Not Reordered With Earlier Loads. Quindi il tuo compilatore sta traducendo correttamente il tuo codice (che sorpresa), in modo tale che il tuo codice sia effettivamente completamente sequenziale e non accada nulla di interessante contemporaneamente.
EOF

Grazie ! (Stavo andando piano in sordina.) FWIW Raccomando link - in particolare la sezione 3, il "Modello del programmatore". Ma per evitare l'errore in cui mi sono imbattuto, nota che in "3.1 The Abstract Machine" ci sono "thread hardware" ognuno dei quali è "un singolo flusso in ordine di esecuzione dell'istruzione" (la mia enfasi è stata aggiunta). Ora posso tornare a cercare di capire lo standard C11 ... con meno dissonanza cognitiva :-)
Chris Hall,

Risposte:


1

Il modello di memoria di x86 è sostanzialmente coerenza sequenziale più un buffer di archivio (con inoltro di archivio). Quindi ogni negozio è un negozio di rilascio 1 . Questo è il motivo per cui solo i negozi seq-cst necessitano di istruzioni speciali. ( Mapping atomici C / C ++ 11 su asm ). Inoltre, https://stackoverflow.com/tags/x86/info ha alcuni collegamenti a documenti x86, tra cui una descrizione formale del modello di memoria x86-TSO (praticamente illeggibile per la maggior parte degli umani; richiede di sfogliare molte definizioni).

Dato che stai già leggendo l'eccellente serie di articoli di Jeff Preshing, ti indicherò un altro che approfondisce: https://preshing.com/20120930/weak-vs-strong-memory-models/

L'unico riordino consentito su x86 è StoreLoad, non LoadStore , se stiamo parlando in questi termini. (L'inoltro del negozio può fare cose extra divertenti se un carico si sovrappone solo parzialmente a un negozio; Istruzioni di caricamento invisibili a livello globale , anche se non lo otterrai mai nel codice generato dal compilatore stdatomic.)

@EOF ha commentato la citazione giusta dal manuale di Intel:

Manuale per gli sviluppatori del software per architetture Intel® 64 e IA-32 Volume 3 (3A, 3B, 3C e 3D): Guida alla programmazione del sistema, 8.2.3.3 I negozi non sono riordinati con carichi precedenti.


Nota 1: ignorare i negozi NT debolmente ordinati; questo è il motivo per cui normalmente sfencedopo aver fatto negozi NT. Le implementazioni C11 / C ++ 11 presuppongono che non si stiano utilizzando gli archivi NT. In _mm_sfencetal caso, utilizzare prima di un'operazione di rilascio per assicurarsi che rispetti i negozi NT. (In generale non usare _mm_mfence/ _mm_sfencein altri casi ; di solito è necessario solo bloccare il riordino in fase di compilazione. O ovviamente usare solo stdatomic.)


Trovo x86-TSO: un modello di programmatore rigoroso e utilizzabile per multiprocessori x86 più leggibile della descrizione formale (correlata) a cui si fa riferimento. Ma la mia vera ambizione è comprendere appieno le sezioni 5.1.2.4 e 7.17.3 della norma C11 / C18. In particolare, penso di ottenere Release / Acquire / Acquire + Release, ma memory_order_seq_cst è definito separatamente e sto lottando per vedere come si incastrano tutti insieme :-(
Chris Hall,

@ChrisHall: ho scoperto che mi ha aiutato a capire esattamente quanto può essere debole acq / rel, e per questo è necessario guardare macchine come POWER in grado di riordinare IRIW. (che seq-cst proibisce ma acq / rel no). Due scritture atomiche in posizioni diverse in thread diversi saranno sempre visualizzate nello stesso ordine da altri thread? . Inoltre, come raggiungere una barriera StoreLoad in C ++ 11? discute di quanto poco lo standard garantisca formalmente l'ordinazione al di fuori dei casi di sychronizes-con o tutto-seq-cst.
Peter Cordes,

@ChrisHall: La cosa principale che fa seq-cst è bloccare il riordino di StoreLoad. (Su x86 è l'unica cosa che fa oltre acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act usa asm, ma equivale a seq-cst vs. acq / rel
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.