Domanda Perché GCC non usa LOAD (senza recinzione) e STORE + SFENCE per coerenza sequenziale?


Ecco quattro approcci per creare coerenza sequenziale in x86 / x86_64:

  1. CARICO (senza recinzione) e STORE + MFENCE
  2. CARICO (senza recinzione) e LOCK XCHG
  3. MFENCE + LOAD e STORE (senza recinzione)
  4. BLOCCA XADD (0) e STORE (senza recinzione)

Come è scritto qui: http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

C / C ++ 11 Operazione implementazione x86

  • Carica Seq_Cst: MOV (dalla memoria)
  • Store Seq Cst: (LOCK) XCHG //   alternativa: MOV (in memoria), MFENCE

Nota: esiste una mappatura alternativa di C / C ++ da 11 a x86, che invece di bloccare (o schermare) l'archivio Seq Cst blocca / blocca il carico di Seq Cst:

  • Carica Seq_Cst: LOCK XADD (0) // alternativa: MFENCE, MOV (dalla memoria)
  • Store Seq Cst: MOV (in memoria)

GCC 4.8.2 (GDB in x86_64) usa il primo (1) approccio per C ++ 11-std :: memory_order_seq_cst, cioè CARICO (senza recinzione) e NEGOZIO + MFENZA:

std::atomic<int> a;
int temp = 0;
a.store(temp, std::memory_order_seq_cst);
0x4613e8  <+0x0058>         mov    0x38(%rsp),%eax
0x4613ec  <+0x005c>         mov    %eax,0x20(%rsp)
0x4613f0  <+0x0060>         mfence

Come sappiamo, quel MFENCE = LFENCE + SFENCE. Quindi questo codice possiamo riscrivere a questo: LOAD(without fence) and STORE+LFENCE+SFENCE

Domande: 

  1. Perché non abbiamo bisogno di usare LFENCE qui prima di LOAD, e dobbiamo usare LFENCE dopo STORE (perché LFENCE ha senso solo prima di LOAD!)?
  2. Perché GCC non usa l'approccio: LOAD (senza fence) e STORE + SFENCE per std :: memory_order_seq_cst?

14
2017-09-27 09:29


origine


risposte:


std::atomic<int>::store è mappato al compilatore intrinseco __atomic_store_n. (Questa e altre intrinseche operazioni atomiche sono documentate qui: Funzioni integrate per operazioni atomiche con memoria del modello di memoria.) Il _n il suffisso lo rende di tipo generico; il back-end implementa effettivamente varianti per dimensioni specifiche in byte. int su x86 è AFAIK sempre lungo 32 bit, quindi significa che stiamo cercando la definizione di __atomic_store_4. Il manuale interno per questa versione di GCC dice che il __atomic_store le operazioni corrispondono ai modelli di descrizione della macchina denominati atomic_store‌modalità; il modalità corrispondente a un intero di 4 byte è "SI" (questo è documentato qui), quindi stiamo cercando qualcosa chiamato "atomic_storesi"nella descrizione della macchina x86. E questo ci porta a config / i386 / sync.md, in particolare questo bit:

(define_expand "atomic_store<mode>"
  [(set (match_operand:ATOMIC 0 "memory_operand")
        (unspec:ATOMIC [(match_operand:ATOMIC 1 "register_operand")
                        (match_operand:SI 2 "const_int_operand")]
                       UNSPEC_MOVA))]
  ""
{
  enum memmodel model = (enum memmodel) (INTVAL (operands[2]) & MEMMODEL_MASK);

  if (<MODE>mode == DImode && !TARGET_64BIT)
    {
      /* For DImode on 32-bit, we can use the FPU to perform the store.  */
      /* Note that while we could perform a cmpxchg8b loop, that turns
         out to be significantly larger than this plus a barrier.  */
      emit_insn (gen_atomic_storedi_fpu
                 (operands[0], operands[1],
                  assign_386_stack_local (DImode, SLOT_TEMP)));
    }
  else
    {
      /* For seq-cst stores, when we lack MFENCE, use XCHG.  */
      if (model == MEMMODEL_SEQ_CST && !(TARGET_64BIT || TARGET_SSE2))
        {
          emit_insn (gen_atomic_exchange<mode> (gen_reg_rtx (<MODE>mode),
                                                operands[0], operands[1],
                                                operands[2]));
          DONE;
        }

      /* Otherwise use a store.  */
      emit_insn (gen_atomic_store<mode>_1 (operands[0], operands[1],
                                           operands[2]));
    }
  /* ... followed by an MFENCE, if required.  */
  if (model == MEMMODEL_SEQ_CST)
    emit_insn (gen_mem_thread_fence (operands[2]));
  DONE;
})

Senza entrare in una grande quantità di dettagli, la maggior parte di questo è un corpo di funzioni C che verrà chiamato per generare il livello basso "RTL"rappresentazione intermedia dell'operazione dell'archivio atomico. Quando viene invocato dal codice di esempio, <MODE>mode != DImode, model == MEMMODEL_SEQ_CST, e TARGET_SSE2 è vero, quindi chiamerà gen_atomic_store<mode>_1 e poi gen_mem_thread_fence. L'ultima funzione genera sempre mfence. (C'è un codice in questo file da produrre sfence, ma credo che sia usato solo per codice esplicito _mm_sfence (a partire dal <xmmintrin.h>).)

I commenti suggeriscono che qualcuno pensato che in questo caso era necessario MFENCE. Ne concludo o ti sbagli a pensare che non sia necessario un recinto di carico, o questo è un errore di ottimizzazione mancato in GCC. È non, ad esempio, un errore nel modo in cui stai usando il compilatore.


5
2017-09-29 19:16



Considera il seguente codice:

#include <atomic>
#include <cstring>

std::atomic<int> a;
char b[64];

void seq() {
  /*
    movl    $0, a(%rip)
    mfence
  */
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
}

void rel() {
  /*
    movl    $0, a(%rip)
   */
  int temp = 0;
  a.store(temp, std::memory_order_relaxed);
}

Per quanto riguarda la variabile atomica "a", seq () e rel () sono sia ordinati che atomici sull'architettura x86 perché:

  1. mov è un'istruzione atomica
  2. mov è un'istruzione legacy e Intel promette la semantica della memoria ordinata affinché le istruzioni legacy siano compatibili con vecchi processori che usavano sempre semantica della memoria ordinata.

Non è richiesto alcun recinto per memorizzare un valore costante in una variabile atomica. Le recinzioni ci sono perché std :: memory_order_seq_cst implica che tutta la memoria è sincronizzata, non solo la memoria che contiene la variabile atomica.

L'effetto può essere dimostrato dal seguente set e ottieni funzioni:

void set(const char *s) {
  strcpy(b, s);
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
}

const char *get() {
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
  return b;
}

strcpy è una funzione di libreria che potrebbe utilizzare le nuove istruzioni sse se sono disponibili in runtime. Poiché le istruzioni sse non erano disponibili nei vecchi processori, non vi è alcun obbligo di compatibilità con le versioni precedenti e l'ordine di memoria non è definito. Quindi il risultato di una strcpy in un thread potrebbe non essere direttamente visibile in altri thread.

Le funzioni set e get precedenti utilizzano un valore atomico per imporre la sincronizzazione della memoria in modo che il risultato di strcpy diventi visibile in altri thread. Ora le recinzioni sono importanti, ma l'ordine di queste all'interno della chiamata a atomic :: store non è significativo poiché le recinzioni non sono necessarie internamente in atomic :: store.


5
2017-09-30 07:12



L'unico riordino x86 (per i normali accessi alla memoria) è che può potenzialmente riordinare un carico che segue un negozio.

SFENCE garantisce che tutti i negozi prima della recinzione siano completi prima di tutti i negozi dopo la recinzione. LFENCE garantisce che tutti i carichi prima della recinzione siano completi prima di tutti i carichi dopo la recinzione. Per gli accessi normali alla memoria, le garanzie di ordinazione delle singole operazioni di SFENCE o LFENCE sono già fornite di default. Fondamentalmente, LFENCE e SFENCE sono utili solo per le deboli modalità di accesso alla memoria di x86.

Né LFENCE, SFENCE, LFENCE + SFENCE impedisce che un negozio seguito da un carico venga riordinato. MFENCE fa.

Il riferimento pertinente è il manuale di architettura Intel x86.


5
2017-10-12 11:30



È SFENCE + LFENCE non una barriera StoreLoad (MFENCE), quindi la premessa della domanda è errata. (Vedi anche la mia risposta su un'altra versione di questa stessa domanda dello stesso utente Perché è (o non è?) SFENCE + LFENCE equivalente a MFENCE?)


  • SFENCE può passare (apparire prima) ai carichi precedenti. (È solo una barriera StoreStore).
  • LFENCE può passare i negozi precedenti. (I carichi non possono attraversarlo in nessuna direzione: LoadLoad barrier).
  • I carichi possono passare a SFENCE (ma i negozi non possono superare LFENCE, quindi è una barriera di LoadStore e una barriera LoadLoad).

LFENCE + SFENCE non include nulla che impedisce a un archivio di essere memorizzato nel buffer fino a dopo un carico successivo. MFENCE fa prevenire questo.

Il blog di Preshingspiega in modo più dettagliato e con diagrammi come le barriere StoreLoad sono speciali e ha un esempio pratico di codice funzionante che dimostra il riordino senza MFENCE. Chiunque sia confuso sull'ordinamento della memoria dovrebbe iniziare con quel blog.

x86 ha un modello di memoria forte dove ogni negozio normale ha semantica di rilascio e ogni carico normale ha acquisito la semantica. Questo post ha i dettagli.

LFENCE e SFENCE esiste solo per l'uso con movnt carichi / negozi, che sono debolmente ordinati oltre a bypassare la cache.


Nel caso in cui quei collegamenti muoiano mai, ci sono ancora più informazioni nella mia rispondi a un'altra domanda simile.


4
2017-09-22 02:09