Acquisire / rilasciare semantica con negozi non temporali su x64

Ho qualcosa come:

if (f = acquire_load() == ) { ... use Foo } 

e:

 auto f = new Foo(); release_store(f) 

Si potrebbe facilmente immaginare un’implementazione di acquire_load e release_store che utilizza atomico con carico (memory_order_acquire) e store (memory_order_release). Ma ora cosa succede se release_store viene implementato con _mm_stream_si64, una scrittura non temporale, che non è ordinata rispetto ad altri negozi su x64? Come ottenere la stessa semantica?

Penso che quanto segue sia il minimo richiesto:

 atomic gFoo; Foo* acquire_load() { return gFoo.load(memory_order_relaxed); } void release_store(Foo* f) { _mm_stream_si64(*(Foo**)&gFoo, f); } 

E usalo in questo modo:

 // thread 1 if (f = acquire_load() == ) { _mm_lfence(); ... use Foo } 

e:

 // thread 2 auto f = new Foo(); _mm_sfence(); // ensures Foo is constructed by the time f is published to gFoo release_store(f) 

È corretto? Sono abbastanza sicuro che la sfence sia assolutamente necessaria qui. Ma per quanto riguarda il lfence? E ‘richiesto o sarebbe sufficiente una semplice barriera per il compilatore per x64? eg asm volatile (“”::: “memoria”). Secondo il modello di memoria x86, i carichi non vengono riordinati con altri carichi. Quindi, per quanto ne so, acquire_load () deve accadere prima di qualsiasi carico all’interno dell’istruzione if, a patto che ci sia una barriera per il compilatore.

Potrei sbagliarmi su alcune cose in questa risposta (la lettura di bozze è benvenuta da persone che conoscono questa roba!). Si basa sulla lettura dei documenti e sul blog di Jeff Preshing, non su esperienze o test recenti.

Linus Torvalds raccomanda vivamente di non inventare il proprio blocco, perché è così facile sbagliarlo. È più un problema quando si scrive codice portatile per il kernel Linux, piuttosto che qualcosa che è solo x86, quindi mi sento abbastanza coraggioso da provare a sistemare le cose per x86.


Il modo normale di usare i negozi NT è di fare un mucchio di loro di seguito, come parte di un memset o memcpy, quindi un SFENCE , quindi un normale archivio di rilascio su una variabile di flag condivisa: done_flag.store(1, std::memory_order_release) .

L’utilizzo di un archivio movnti nella variabile di sincronizzazione danneggia le prestazioni. Potresti voler usare i negozi NT nel Foo cui punta, ma sfrattare il puntatore stesso dalla cache è perverso. ( movnt negozi movnt la linea della cache se era nella cache per iniziare , vedi vol1 cap 10.4.6.2 Memorizzazione nella cache dei dati temporali e non temporali ).

L’intero punto dei negozi NT è per l’uso con dati non temporali, che non saranno più utilizzati (da qualsiasi thread) per molto tempo, se mai. I blocchi che controllano l’accesso ai buffer condivisi, o le bandiere che i produttori / consumatori usano per contrassegnare i dati come letti, dovrebbero essere letti da altri core.

Anche i nomi delle tue funzioni non riflettono realmente ciò che stai facendo.

L’hardware x86 è estremamente ottimizzato per eseguire release-store normali (non NT), perché ogni store normale è un release-store. L’hardware deve essere bravo a fare in modo che x86 funzioni velocemente.

L’utilizzo di normali negozi / carichi richiede solo un intervento sulla cache L3, non su DRAM, per la comunicazione tra i thread sulle CPU Intel. La grande cache L3 di Intel funziona come blocco di sicurezza per il traffico di coerenza della cache. Sondare i tag L3 su una miss da un core rileverà il fatto che un altro core ha la linea cache nello stato Modified o Exclusive . Gli archivi NT richiederebbero che le variabili di sincronizzazione arrivino fino alla DRAM e che vengano restituite a un altro core per vederle.


Ordinamento della memoria per i negozi di streaming NT

movnt negozi movnt possono essere riordinati con altri negozi, ma non con le letture precedenti.

Manuale Intel x86 vol3, capitolo 8.2.2 (Ordinamento della memoria in P6 e Famiglie di processori più recenti) :

  • Le letture non sono riordinate con altre letture.
  • Le scritture non sono riordinate con letture più vecchie . (notare la mancanza di eccezioni).
  • Le scritture in memoria non vengono riordinate con altre scritture, con le seguenti eccezioni:
    • archivi di streaming (scritture) eseguiti con le istruzioni di spostamento non temporali (MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS e MOVNTPD); e
    • operazioni con le stringhe (consultare la Sezione 8.2.4.1). (nota: dalla mia lettura dei documenti, le stringhe veloci e le operazioni ERMSB hanno ancora implicitamente una barriera StoreStore all’inizio / fine . C’è solo un potenziale riordino tra i negozi all’interno di una singola rep movs o rep stos .)
  • … roba su clflushopt e le istruzioni del recinto

aggiornamento: c’è anche una nota (in 8.1.2.2 Blocco del bus controllato da software ) che dice:

Non implementare i semafori usando il tipo di memoria WC. Non eseguire negozi non temporali su una linea della cache contenente un percorso utilizzato per implementare un semaforo.

Questo potrebbe essere solo un suggerimento sul rendimento; non spiegano se può causare un problema di correttezza. Si noti che gli archivi NT non sono coerenti con la cache, tuttavia (i dati possono essere inseriti nel buffer di riempimento riga anche se i dati in conflitto per la stessa riga sono presenti altrove nel sistema o in memoria). Forse potresti tranquillamente usare gli store NT come un release-store che si sincronizza con i carichi regolari, ma si verificherebbero problemi con le operazioni di RMW atomiche come lock add dword [mem], 1 .


La semantica di rilascio impedisce il riordino della memoria del rilascio di scrittura con qualsiasi operazione di lettura o scrittura che lo preceda nell’ordine di programma.

Per bloccare il riordino con i negozi precedenti, abbiamo bisogno di un’istruzione SFENCE , che è una barriera StoreStore anche per i negozi NT. (Ed è anche un ostacolo a qualche tipo di riordino in fase di compilazione, ma non sono sicuro che blocchi i carichi precedenti dall’attraversare la barriera.) I negozi normali non hanno bisogno di alcun tipo di istruzione barriera per essere rilasciati, quindi hai solo bisogno di SFENCE quando usi negozi NT.

Per i carichi: il modello di memoria x86 per la memoria WB (write-back, cioè “normale”) previene già il riordinamento di LoadStore anche per i negozi debolmente ordinati, quindi non abbiamo bisogno di LFENCE per il suo effetto di barriera LoadStore , solo una barriera del compilatore LoadStore prima del negozio NT. Nella implementazione di gcc almeno, std::atomic_signal_fence(std::memory_order_release) è una barriera di compilazione anche per carichi / negozi non atomici, ma atomic_thread_fence è solo una barriera per carichi atomic<> / negozi (incluso mo_relaxed ). L’utilizzo di un atomic_thread_fence consente comunque al compilatore più libertà di riordinare carichi / negozi a variabili non condivise. Vedi questo Q & A per ulteriori informazioni .

 // The function can't be called release_store unless it actually is one (ie includes all necessary barriers) // Your original function should be called relaxed_store void NT_release_store(const Foo* f) { // _mm_lfence(); // make sure all reads from the locked region are already globally visible. Not needed: this is already guaranteed std::atomic_thread_fence(std::memory_order_release); // no insns emitted on x86 (since it assumes no NT stores), but still a compiler barrier for earlier atomic<> ops _mm_sfence(); // make sure all writes to the locked region are already globally visible, and don't reorder with the NT store _mm_stream_si64((long long int*)&gFoo, (int64_t)f); } 

Questo memorizza la variabile atomica (notare la mancanza di dereferenziazione &gFoo ). La tua funzione memorizza il Foo cui punta, che è super strano; IDK quale fosse il punto. Si noti inoltre che viene compilato come codice C ++ 11 valido .

Quando pensi a cosa significa un negozio di versioni, pensa a questo come al negozio che rilascia il blocco su una struttura di dati condivisa. Nel tuo caso, quando il rilascio-store diventa globalmente visibile, qualsiasi thread che lo vede dovrebbe essere in grado di dereferenziarlo in sicurezza.


Per fare un acquis-load, dillo al compilatore che vuoi.

x86 non ha bisogno di istruzioni di barriera, ma la specifica di mo_acquire invece di mo_relaxed offre la necessaria barriera del compilatore. Come bonus, questa funzione è portatile: otterrai tutte le barriere necessarie su altre architetture:

 Foo* acquire_load() { return gFoo.load(std::memory_order_acquire); } 

Non hai detto nulla gFoo di gFoo nella gFoo del WC debolmente ordinato (combinazione di scritture non memorizzabile ) . Probabilmente è molto difficile organizzare il segmento di dati del tuo programma in modo che sia mappato nella memoria del WC … Sarebbe molto più facile per gFoo puntare semplicemente alla memoria del WC, dopo che avrai memorizzato qualche RAM del video WC o qualcosa del genere. Ma se vuoi acquisire dei carichi dalla memoria del WC, probabilmente hai bisogno di LFENCE . IDK. Fai un’altra domanda al riguardo, perché questa risposta presume in gran parte che stai usando la memoria WB.

Si noti che l’utilizzo di un puntatore anziché di un flag crea una dipendenza dai dati. Penso che dovresti essere in grado di usare gFoo.load(std::memory_order_consume) , che non richiede barriere anche su CPU debolmente ordinate (diverse da Alpha). Una volta che i compilatori sono sufficientemente avanzati per assicurarsi che non interrompano la dipendenza dai dati, possono effettivamente creare un codice migliore (invece di promuovere mo_consume su mo_acquire . Leggere questo mo_consume prima di utilizzare mo_consume nel codice di produzione ed esp. testarlo correttamente è imansible perché i futuri compilatori dovrebbero fornire garanzie più deboli rispetto agli attuali compilatori.


Inizialmente pensavo che avessimo bisogno di LFENCE per ottenere una barriera di LoadStore. (“Le scritture non possono passare prima le istruzioni LFENCE, SFENCE e MFENCE”. Ciò a sua volta impedisce loro di passare (diventando visibili globalmente prima) letture che si trovano prima di LFENCE).

Nota che LFENCE + SFENCE è ancora più debole di un MFENCE completo, perché non è una barriera StoreLoad. La documentazione di SFENCE dice che è stata ordinata wrt. LFENCE, ma quella tabella del modello di memoria x86 da vol3 manuale Intel non lo menziona. Se SFENCE non può essere eseguito fino a dopo un LFENCE, quindi sfence / lfence potrebbe effettivamente essere un equivalente più lento di mfence , ma lfence / sfence / movnti fornirebbe semantica di rilascio senza una barriera completa. Si noti che l’archivio NT potrebbe diventare globalmente visibile dopo alcuni carichi / negozi successivi, a differenza di un normale negozio x86 fortemente ordinato.)


Correlati: carichi NT

In x86, ogni carico ha acquisito la semantica, ad eccezione dei carichi dalla memoria del WC. SSE4.1 MOVNTDQA è l’unica istruzione di caricamento non temporale e non è debolmente ordinata quando viene utilizzata nella memoria normale (WriteBack). Quindi è anche un carico di acquisizione (se usato su memoria WB).

Nota che movntdq ha solo un modulo di negozio, mentre movntdqa ha solo un modulo di caricamento. Ma a quanto pare Intel non poteva semplicemente chiamarli storentdqa e loadntdqa . Entrambi hanno un requisito di allineamento 16B o 32B, quindi lasciare l’ a non ha molto senso per me. Suppongo che SSE1 e SSE2 avessero già introdotto alcuni negozi NT già usando il mov... mnemonico (come movntps ), ma nessun carico fino a anni dopo in SSE4.1. (2nd-gen Core2: 45nm Penryn).

I documenti dicono che MOVNTDQA non cambia la semantica degli ordini per il tipo di memoria su cui è usato .

… Un’implementazione può anche utilizzare il suggerimento non temporale associato a questa istruzione se la sorgente di memoria è di tipo WB (write back).

L’ implementazione di un suggerimento non temporale da parte di un processore non sovrascrive la semantica del tipo di memoria effettiva , ma l’implementazione del suggerimento dipende dal processore. Ad esempio, un’implementazione di un processore può scegliere di ignorare il suggerimento ed elaborare l’istruzione come un normale MOVDQA per qualsiasi tipo di memoria.

In pratica, le attuali CPU Intel mainstream (Haswell, Skylake) sembrano ignorare il suggerimento per i carichi PREFETCHNTA e MOVNTDQA dalla memoria WB . Vedi Le architetture x86 correnti supportano carichi non temporali (dalla memoria “normale”)? e anche i carichi non temporali e il prefetcher dell’hardware, funzionano insieme? per ulteriori dettagli.


Inoltre, se lo si utilizza nella memoria WC (ad esempio, copia dalla RAM video, come in questa guida Intel ):

Poiché il protocollo WC utilizza un modello di consistenza di memoria debolmente ordinato, è necessario utilizzare un’istruzione MFENCE o bloccata insieme alle istruzioni MOVNTDQA se più processori possono fare riferimento alle stesse posizioni di memoria WC o per sincronizzare le letture di un processore con scritture da altri agenti nel sistema.

Questo non spiega come dovrebbe essere usato, però. E non sono sicuro del motivo per cui dicono MFENCE piuttosto che LFENCE per la lettura. Forse stanno parlando di una memoria write-to-device, di una memoria read-from-device in cui i negozi devono essere ordinati rispetto ai carichi (barriera StoreLoad), non solo l’uno con l’altro (barriera StoreStore).

Ho cercato in Vol3 per movntdqa e non ho ricevuto alcun hit (nel pdf completo). 3 successi per movntdq : tutta la discussione su ordini deboli e tipi di memoria parla solo di negozi. Si noti che LFENCE stato introdotto molto prima di SSE4.1. Presumibilmente è utile per qualcosa, ma IDK cosa. Per l’ordine di caricamento, probabilmente solo con la memoria del WC, ma non ho letto quando sarebbe utile.


LFENCE sembra essere più di una semplice barriera LoadLoad per carichi debolmente ordinati: ordina anche altre istruzioni. (Non la visibilità globale dei negozi, però, solo la loro esecuzione locale).

Dal manuale di Intel insn ref:

Nello specifico, LFENCE non viene eseguito finché tutte le istruzioni precedenti non sono state completate localmente e nessuna istruzione successiva inizia l’esecuzione fino al completamento di LFENCE.

Le istruzioni che seguono un LFENCE possono essere recuperate dalla memoria prima di LFENCE, ma non verranno eseguite fino al completamento di LFENCE.

La voce per rdtsc suggerisce di usare LFENCE;RDTSC per impedire che venga eseguito prima delle istruzioni precedenti, quando RDTSCP non è disponibile (e la garanzia di ordine più debole è ok: rdtscp non smette di seguire le istruzioni da eseguire prima di esso). ( CPUID è un suggerimento comune per serializzare il stream di istruzioni attorno a rdtsc ).