Cosa dovrebbe sapere ogni programmatore sulla memoria?

Mi chiedo quanto sia ancora valido il contenuto di What Every Programmer di Ulrich Drepper sulla memoria del 2007. Inoltre non sono riuscito a trovare una versione più recente di 1.0 o un errata.

Per quanto mi ricordi, il contenuto di Drepper descrive concetti fondamentali sulla memoria: come funziona la cache della CPU, quali sono la memoria fisica e virtuale e come il kernel di Linux gestisce questo zoo. Probabilmente ci sono riferimenti API obsoleti in alcuni esempi, ma non importa; ciò non influirà sulla rilevanza dei concetti fondamentali.

Quindi, qualsiasi libro o articolo che descrive qualcosa di fondamentale non può essere definito obsoleto. “Quello che ogni programmatore dovrebbe sapere sulla memoria” è sicuramente la pena di leggere, ma, beh, non penso che sia per “ogni programmatore”. È più adatto per i ragazzi di sistema / embedded / kernel.

Dalla mia rapida occhiata, sembra abbastanza accurato. L’unica cosa da notare è la porzione sulla differenza tra controller di memoria “integrati” e “esterni”. Fin dal rilascio della linea i7, le CPU Intel sono tutte integrate e AMD ha utilizzato i controller di memoria integrati da quando i chip AMD64 sono stati rilasciati per la prima volta.

Dal momento che 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 in RAM fino a quando non si sentirà come commettere le modifiche), ma non tutto è cambiato . Almeno non in alcun modo che uno sviluppatore di software avrebbe a cuore.

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) avesse scritto un aggiornamento del 2017, ma sarebbe stato molto impegnativo (ad esempio, rieseguire i benchmark). Vedi anche altri x86 link di ottimizzazione delle prestazioni e SSE / asm (e C / C ++) nel wiki dei tag x86 . (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 funzionano ancora . DDR4 utilizza gli stessi comandi descritti per DDR1 / DDR2 (burst di lettura / scrittura). I miglioramenti DDR3 / 4 non sono cambiamenti fondamentali. AFAIK, tutte le cose indipendenti dall’archivio si applicano ancora in generale, ad es. Per AArch64 / ARM32.

Vedi anche la sezione Latency Bound Platforms di questa risposta per dettagli importanti sull’effetto della memoria / latenza L3 su larghezza di banda a thread singolo: bandwidth <= max_concurrency / latency e questo è in realtà il collo di bottiglia principale per larghezza di banda a thread singolo su un numero moderno -core CPU come un Xeon. (Ma un desktop Skylake quad-core può avvicinarsi al massimo della larghezza di banda DRAM con un singolo thread). Quel link ha alcune ottime informazioni sugli store NT rispetto ai normali negozi su x86.

Quindi il suggerimento di Ulrich in 6.5.8 Utilizzare tutta la larghezza di banda (usando la memoria remota su altri nodes NUMA e il proprio) è controproducente su hardware moderno in cui i controller di memoria hanno più larghezza di banda di un singolo core. Probabilmente puoi immaginare una situazione in cui ci sono alcuni vantaggi nell'esecuzione di più thread affamati di memoria sullo stesso nodo NUMA per comunicazioni inter-thread a bassa latenza, ma che li facciano usare memoria remota per roba sensibile alla latenza elevata a larghezza di banda elevata. Ma questo è piuttosto oscuro; di solito invece di utilizzare intenzionalmente la memoria remota quando si poteva usare locale, basta dividere i thread tra i nodes NUMA e farli usare la memoria locale.


(di solito) Non utilizzare il software di precaricamento

Una cosa importante che è cambiata è che il prefetch hardware è molto meglio che su P4 e in grado di riconoscere i modelli di accesso a passi stretti fino a un passo abbastanza grande e più flussi contemporaneamente (ad esempio, uno avanti / indietro per una pagina di 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 più tardi hanno il prefetch hardware della pagina successiva, invece di aspettare una mancanza della cache nella nuova pagina per triggersre un avvio veloce. (Suppongo 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 buoni solo per P4. Le sezioni specifiche di Sandybridge sono ovviamente accurate per SnB, ma ad esempio la non laminazione degli uops micro-fusi cambiati in HSW e il manuale non ne parla .

Il solito consiglio in questi giorni è di rimuovere tutto il prefetch SW dal vecchio codice , e prendere in considerazione la possibilità di rimetterlo in funzione se la profilatura mostra errori di cache (e non si sta saturando la larghezza di banda della memoria). Il prefetching di entrambi i lati del prossimo passo di una ricerca binaria può ancora aiutare. Ad esempio, una volta che decidi quale elemento guardare in seguito, prelevi gli elementi 1/4 e 3/4 in modo che possano essere caricati parallelamente al caricamento / controllo medio.

Il suggerimento di usare un thread prefetch separato (6.3.4) è del tutto obsoleto , penso, ed è stato sempre buono solo su Pentium 4. P4 ha avuto hyperthreading (2 core logici che condividono un core fisico), ma non abbastanza out-of-order risorse di esecuzione o trace-cache per ottenere un throughput con due thread di computazione completi sullo stesso core. Ma le CPU moderne (Sandybridge-family e Ryzen) sono molto più resistenti e dovrebbero 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).

Il precaricamento del software è sempre stato "fragile" : i giusti numeri di ottimizzazione della magia per ottenere una velocità dipendono dai dettagli dell'hardware e forse dal carico del sistema. Troppo presto e viene sfrattato prima del carico della domanda. Troppo tardi e non aiuta. Questo articolo del blog mostra il codice + i grafici per un interessante esperimento sull'uso del prefetch SW su Haswell per il prefetching della parte non sequenziale di un problema. Vedi anche Come usare correttamente le istruzioni di prefetch? . NT prefetch è interessante, ma ancora più fragile (perché uno sfratto anticipato da L1 significa che devi andare fino a L3 o DRAM, non solo a L2). Se hai bisogno dell'ultima goccia di prestazioni, e puoi sintonizzarti su una macchina specifica, SW prelettura merita di essere consultato per l'accesso sequenziale, ma se può ancora essere un rallentamento se hai abbastanza lavoro di ALU da fare mentre ti avvicini ai bottlenecking in memoria .


La dimensione della linea cache è ancora 64 byte. (La larghezza di banda di lettura / scrittura L1D è molto alta e le CPU moderne possono eseguire 2 carichi vettoriali per clock + 1 vettore se tutti colpiscono in L1D. Vedi Come può essere la cache così veloce? ) Con AVX512, dimensione linea = larghezza vettore, in modo che tu possa caricare / memorizzare un'intera linea della cache in un'unica istruzione. (E quindi ogni carico / archivio disallineato attraversa un limite della linea cache, invece di ogni altro per 256b AVX1 / AVX2, che spesso non rallenta il loop su un array che non era in L1D.)

Le istruzioni di caricamento non allineate hanno zero penalità se l'indirizzo è allineato in fase di runtime, ma i compilatori (specialmente gcc) creano un codice migliore quando si autovettionano se sanno di eventuali garanzie di allineamento. Le operazioni effettivamente non allineate sono generalmente veloci, ma le suddivisioni di pagina fanno ancora male (molto meno su Skylake, tuttavia: solo ~ 11 cicli extra di latenza rispetto a 100, ma ancora una penalità di throughput).


Come previsto da Ulrich, oggigiorno ogni sistema multi-socket è NUMA: i controller di memoria integrati sono standard, ovvero non esiste un Northbridge esterno. Ma SMP non significa più multi-socket, perché le CPU multi-core sono diffuse. (Le CPU Intel da Nehalem a Skylake hanno utilizzato una grande cache L3 inclusa come supporto 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 permette di controllare ciò che è memorizzato nella cache ovunque sul chip (e in tal caso dove) senza effettivamente trasmettere snoop a tutti i core. SKX utilizza una mesh piuttosto che un ring bus , con una latenza generalmente peggiore dei precedenti Xeon multi-core, sfortunatamente.

Fondamentalmente tutti i consigli sull'ottimizzazione del posizionamento della memoria sono ancora validi, solo i dettagli di esattamente cosa succede quando non è ansible evitare errori di cache o contesa.


6.4.2 Operazioni atomiche : il benchmark che mostra un loop CAS-retry come 4x peggiore rispetto lock add arbitrato all'hardware probabilmente riflette ancora un caso di contesa massimo . Ma nei programmi multi-thread reali, la sincronizzazione è ridotta al minimo (perché è costosa), quindi la contesa è bassa e un ciclo CAS-retry di solito riesce senza dover riprovare.

C ++ 11 std::atomic fetch_add si compila in un lock add (o lock xadd se viene usato il valore di ritorno), ma un algoritmo che usa CAS per fare qualcosa che non può essere fatto con un'istruzione lock ed è di solito non un disastro. Usa C ++ 11 std::atomic o C11 stdatomic invece di gcc legacy __sync built-in o i più recenti __atomic built-in a meno che tu non voglia unire l'accesso atomico e non atomico alla stessa posizione ...

8.1 DCAS ( cmpxchg16b ) : puoi convincere gcc ad cmpxchg16b , ma se vuoi carichi efficienti di solo una metà dell'object, hai bisogno di brutti legami union : come posso implementare il contatore ABA con c ++ 11 CAS?

8.2.4 memoria transazionale : dopo un paio di false partenze (rilasciate poi disabilitate da un aggiornamento del microcodice a causa di un bug raramente triggersto), Intel ha funzionato la memoria transazionale nel Broadwell del modello successivo e tutte le CPU Skylake. Il design è ancora ciò che David Kanter ha descritto per Haswell . C'è un modo lock-ellision per usarlo per accelerare il codice che usa (e può ricorrere a) un blocco regolare (specialmente con un singolo blocco 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 Pagine enormi: le enormi pagine anonime trasparenti funzionano bene su Linux senza dover utilizzare manualmente hugetlbfs. Rendi allocazioni> = 2MiB con allineamento 2MiB (ad es. posix_memalign o un aligned_alloc che non impone lo stupido requisito ISO C ++ 17 di fallire quando l' size % alignment != 0 ).

Per impostazione predefinita, un'allocazione anonima allineata con 2MiB utilizzerà le pagine enormi. Alcuni carichi di lavoro (ad esempio, che continuano a utilizzare grandi allocazioni per un po 'dopo averli creati) potrebbero trarne beneficio
echo always >/sys/kernel/mm/transparent_hugepage/defrag per ottenere il kernel per deframmentare la memoria fisica ogni volta che è necessario, invece di ricorrere a 4k pagine. (Vedi i documenti del kernel ). In alternativa, utilizzare madvise(MADV_HUGEPAGE) dopo aver effettuato allocazioni di grandi dimensioni (preferibilmente ancora con allineamento 2MiB).


Appendice B: Oprofile : Linux perf ha per lo più superato oprofile . Per eventi dettagliati specifici di alcune microarchitetture, usa il wrapper ocperf.py . 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 MOV di x86 può essere veramente "libero"? Perché non posso riprodurre questo a tutti? .