Mi chiedo quanta parte di Ulrich Drepper di ciò che ogni programmatore dovrebbe sapere sulla memoria del 2007 sia ancora valida. Inoltre non sono riuscito a trovare una versione più recente di 1.0 o errata.
Mi chiedo quanta parte di Ulrich Drepper di ciò che ogni programmatore dovrebbe sapere sulla memoria del 2007 sia ancora valida. Inoltre non sono riuscito a trovare una versione più recente di 1.0 o errata.
Risposte:
Per quanto ricordo il contenuto di Drepper descrive concetti fondamentali sulla memoria: come funziona la cache della CPU, cosa sono la memoria fisica e virtuale e come il kernel Linux gestisce quello zoo. Probabilmente ci sono riferimenti API obsoleti in alcuni esempi, ma non importa; ciò non influirà sulla pertinenza dei concetti fondamentali.
Quindi, qualsiasi libro o articolo che descrive qualcosa di fondamentale non può essere definito obsoleto. "Ciò che ogni programmatore dovrebbe sapere sulla memoria" merita sicuramente di essere letto, ma, beh, non credo che sia per "ogni programmatore". È più adatto per i ragazzi del sistema / embedded / kernel.
La guida in formato PDF è disponibile su https://www.akkadia.org/drepper/cpumemory.pdf .
È ancora generalmente eccellente e altamente raccomandato (da me, e penso da altri esperti di ottimizzazione delle prestazioni). Sarebbe bello se Ulrich (o chiunque altro) scrivesse un aggiornamento del 2017, ma sarebbe un sacco di lavoro (ad esempio rieseguire i benchmark). Vedi anche altri link di ottimizzazione delle prestazioni x86 e di ottimizzazione SSE / asm (e C / C ++) inX 86 tag wiki . (L'articolo di Ulrich non è specifico per x86, ma la maggior parte (tutti) dei suoi benchmark sono su hardware x86.)
I dettagli hardware di basso livello su come funzionano DRAM e cache sono ancora validi . DDR4 utilizza gli stessi comandi descritti per DDR1 / DDR2 (lettura / scrittura a raffica). I miglioramenti di DDR3 / 4 non sono cambiamenti fondamentali. AFAIK, tutte le cose indipendenti dall'arco si applicano ancora in generale, ad esempio ad AArch64 / ARM32.
Vedi anche la sezione Piattaforme legate alla latenza di questa risposta per dettagli importanti sull'effetto della memoria / latenza L3 sulla larghezza di banda a thread singolo: bandwidth <= max_concurrency / latency
e questo è in realtà il principale collo di bottiglia per la larghezza di banda a thread singolo su una moderna CPU a molti core come un Xeon . Ma un desktop quad-core Skylake può avvicinarsi al massimo della larghezza di banda DRAM con un singolo thread. Questo link ha alcune informazioni molto buone sui negozi NT rispetto ai normali negozi su x86. Perché Skylake è molto meglio di Broadwell-E per il throughput di memoria a thread singolo? è un riassunto.
Pertanto il suggerimento di Ulrich in 6.5.8 L'utilizzo di tutta la larghezza di banda sull'uso della memoria remota su altri nodi NUMA e sul proprio, è controproducente su hardware moderno in cui i controller di memoria hanno una larghezza di banda maggiore di quella che un singolo core può usare. Beh, forse puoi immaginare una situazione in cui c'è un netto vantaggio nell'esecuzione di più thread affamati di memoria sullo stesso nodo NUMA per comunicazioni inter-thread a bassa latenza, ma utilizzandoli usano la memoria remota per roba non sensibile alla latenza ad alta larghezza di banda. Ma questo è piuttosto oscuro, normalmente dividi i thread tra i nodi NUMA e fai loro usare la memoria locale. La larghezza di banda per core è sensibile alla latenza a causa dei limiti di concorrenza massima (vedere di seguito), ma tutti i core in un socket di solito possono più che saturare i controller di memoria in quel socket.
Una cosa importante che è cambiata è che il prefetch hardware è molto meglio che sul Pentium 4 e può riconoscere schemi di accesso a passo lungo fino a un passo abbastanza grande e più flussi contemporaneamente (ad esempio uno avanti / indietro per pagina 4k). Il manuale di ottimizzazione di Intel descrive alcuni dettagli dei prefetcher HW in vari livelli di cache per la loro microarchitettura della famiglia Sandybridge. Ivybridge e versioni successive hanno il prefetch hardware della pagina successiva, invece di attendere il mancato avvio della cache nella nuova pagina per avviare un avvio rapido. Presumo che AMD abbia alcune cose simili nel loro manuale di ottimizzazione. Attenzione che il manuale di Intel è anche pieno di vecchi consigli, alcuni dei quali sono validi solo per P4. Le sezioni specifiche di Sandybridge sono ovviamente accurate per SnB, ma ad esla non laminazione di uops microfusi è cambiata in HSW e il manuale non lo menziona .
Il consueto consiglio in questi giorni è quello di rimuovere tutto il prefetch SW dal vecchio codice e considerare di rimetterlo solo se il profiling mostra mancati cache (e non stai saturando la larghezza di banda della memoria). Il prefetch di entrambi i lati del passaggio successivo di una ricerca binaria può comunque essere d'aiuto. ad es. una volta che hai deciso quale elemento guardare dopo, precarica gli elementi 1/4 e 3/4 in modo che possano caricarsi in parallelo con il caricamento / controllo centrale.
Il suggerimento di utilizzare un thread prefetch separato (6.3.4) è del tutto obsoleto , penso, ed è sempre stato buono su Pentium 4. P4 aveva hyperthreading (2 core logici che condividevano un core fisico), ma non abbastanza trace-cache (e / o risorse di esecuzione fuori ordine) per ottenere throughput eseguendo due thread di calcolo completi sullo stesso core. Ma le moderne CPU (famiglia Sandybridge e Ryzen) sono molto più potenti e dovrebbero o eseguire un thread reale o non utilizzare l'hyperthreading (lasciare l'altro core logico inattivo in modo che il thread solo abbia le risorse complete invece di partizionare il ROB).
Il prefetch del software è sempre stato "fragile" : i giusti numeri di ottimizzazione magica per ottenere uno speedup dipendono dai dettagli dell'hardware e forse dal caricamento del sistema. Troppo presto ed è sfrattato prima del carico della domanda. Troppo tardi e non aiuta. Questo articolo di blog mostra codice + grafici per un interessante esperimento sull'uso del prefetch SW su Haswell per il prefetch della parte non sequenziale di un problema. Vedi anche Come utilizzare correttamente le istruzioni di prefetch? . Il prefetch NT è interessante, ma ancora più fragile perché uno sfratto iniziale da L1 significa che devi andare fino a L3 o DRAM, non solo L2. Se hai bisogno di tutte le ultime prestazioni e puoi sintonizzarti su una macchina specifica, vale la pena cercare il prefetch SW per l'accesso sequenziale, mapotrebbe comunque essere un rallentamento se hai abbastanza lavoro ALU da fare mentre ti avvicini al collo di bottiglia in memoria.
La dimensione della linea della cache è ancora di 64 byte. (La larghezza di banda di lettura / scrittura di L1D è molto elevata e le moderne CPU possono fare 2 carichi vettoriali per clock + 1 archivio vettoriale se tutto colpisce in L1D. Vedi Come può la cache essere così veloce?. ) Con AVX512, dimensione della linea = larghezza del vettore, in modo da poter caricare / archiviare un'intera riga della cache in un'unica istruzione. Pertanto, ogni carico / archivio disallineato attraversa un limite della linea di cache, anziché un altro per AVX1 / AVX2 a 256 b, che spesso non rallenta il looping su un array che non era in L1D.
Le istruzioni di caricamento non allineate hanno una penalità pari a zero se l'indirizzo è allineato in fase di esecuzione, ma i compilatori (in particolare gcc) rendono il codice migliore durante l'autovectorizzazione se conoscono eventuali garanzie di allineamento. Le operazioni in realtà non allineate sono generalmente veloci, ma le suddivisioni di pagina fanno ancora male (molto meno su Skylake, tuttavia; solo ~ 11 cicli di latenza in più rispetto a 100, ma comunque una penalità di throughput).
Come previsto da Ulrich, ogni sistema multi-socket è NUMA in questi giorni: i controller di memoria integrati sono standard, ovvero non esiste un Northbridge esterno. Ma SMP non significa più multi-socket, poiché le CPU multi-core sono molto diffuse. Le CPU Intel da Nehalem a Skylake hanno utilizzato una grande cache L3 inclusiva come backstop per la coerenza tra i core. Le CPU AMD sono diverse, ma non sono così chiaro nei dettagli.
Skylake-X (AVX512) non ha più un L3 inclusivo, ma penso che ci sia ancora una directory di tag che consente di controllare cosa è memorizzato nella cache in qualsiasi punto del chip (e in tal caso dove) senza effettivamente trasmettere ficcanaso a tutti i core. SKX usa una mesh piuttosto che un ring bus , con una latenza generalmente ancora peggiore rispetto ai precedenti Xeon a molti core, sfortunatamente.
Fondamentalmente, tutti i consigli sull'ottimizzazione del posizionamento della memoria sono ancora validi, solo i dettagli su cosa succede esattamente quando non si possono evitare errori nella cache o contese variano.
6.4.2 Operazioni atomiche : il benchmark che mostra un loop di tentativi CAS come 4x peggiore di quello arbitrato dall'hardware lock add
probabilmente riflette ancora un caso di contesa massimo . Ma in veri programmi multi-thread, la sincronizzazione è ridotta al minimo (perché è costosa), quindi la contesa è bassa e un ciclo di tentativi CAS di solito ha successo senza dover riprovare.
C ++ 11 std::atomic
fetch_add
verrà compilato in un lock add
(o lock xadd
se viene utilizzato il valore restituito), ma un algoritmo che utilizza CAS per fare qualcosa che non può essere fatto con lock
un'istruzione ed di solito non è un disastro. Usa C ++ 11std::atomic
o C11 stdatomic
invece dei __sync
built-in legacy gcc o dei nuovi __atomic
built-in a meno che tu non voglia mescolare l'accesso atomico e non atomico nella stessa posizione ...
8.1 DWCAS ( cmpxchg16b
) : puoi convincere gcc a emetterlo , ma se vuoi carichi efficienti di solo metà dell'oggetto, hai bisogno di brutti union
hack: come posso implementare il contatore ABA con c ++ 11 CAS? . (Non confondere DWCAS con DCAS di 2 posizioni di memoria separate . L'emulazione atomica senza DCAS di DCAS non è possibile con DWCAS, ma la memoria transazionale (come x86 TSX) lo rende possibile.)
8.2.4 Memoria transazionale : dopo un paio di false avvii (rilasciati e poi disabilitati da un aggiornamento del microcodice a causa di un bug raramente innescato), Intel ha memoria transazionale funzionante nel Broadwell modello tardivo e in tutte le CPU Skylake. Il design è ancora quello che David Kanter ha descritto per Haswell . Esiste un modo di blocco-ellisse per usarlo per velocizzare il codice che utilizza (e può ricorrere a) un blocco regolare (specialmente con un blocco singolo per tutti gli elementi di un contenitore in modo che più thread nella stessa sezione critica spesso non si scontrino ) o per scrivere codice che conosca direttamente le transazioni.
7.5 Hugepages : hugepage trasparenti anonimi funzionano bene su Linux senza dover utilizzare manualmente hugetlbfs. Effettua allocazioni> = 2MiB con allineamento di 2MiB (ad esempio posix_memalign
, o unaligned_alloc
che non impone che lo stupido requisito ISO C ++ 17 fallisca quando size % alignment != 0
).
Un'allocazione anonima allineata a 2MiB utilizzerà le hugepage per impostazione predefinita. Alcuni carichi di lavoro (ad es. Che continuano a utilizzare allocazioni di grandi dimensioni per un po 'dopo averli creati) possono trarre vantaggio dal
echo always >/sys/kernel/mm/transparent_hugepage/defrag
kernel per deframmentare la memoria fisica ogni volta che è necessario, invece di ricorrere a pagine 4K. (Vedi i documenti del kernel ). In alternativa, utilizzare madvise(MADV_HUGEPAGE)
dopo aver effettuato grandi allocazioni (preferibilmente ancora con allineamento di 2 MiB).
Appendice B: Oprofile : Linux perf
ha sostituito per lo più oprofile
. Per eventi dettagliati specifici di determinate microarchitettura, utilizzare il ocperf.py
wrapper . per esempio
ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out
Per alcuni esempi di utilizzo, vedi Il MOV di x86 può davvero essere "libero"? Perché non riesco a riprodurlo affatto? .
Dal mio rapido sguardo sembra abbastanza preciso. L'unica cosa da notare è la parte sulla differenza tra controller di memoria "integrati" e "esterni". Sin dal rilascio della linea i7, le CPU Intel sono tutte integrate e AMD utilizza controller di memoria integrati da quando sono stati rilasciati i chip AMD64.
Da quando questo articolo è stato scritto, non è cambiato molto, le velocità sono aumentate, i controller di memoria sono diventati molto più intelligenti (l'i7 ritarderà le scritture su RAM fino a quando non si avrà la sensazione di eseguire le modifiche), ma non è cambiato molto . Almeno non in alcun modo a cui dovrebbe interessare uno sviluppatore software.