Che cosa dovrebbe sapere ogni programmatore sulla memoria?


165

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.


1
qualcuno sa se posso scaricare questo articolo in formato mobi da qualche parte in modo da poterlo leggere facilmente su kindle? "pdf" è molto difficile da leggere a causa di problemi con zoom / formattazione
javapowered

1
Non è mobi, ma LWN ha pubblicato il documento come una serie di articoli che sono più facili da leggere su un telefono / tablet. Il primo è su lwn.net/Articles/250967
Nathan

Risposte:


111

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.


3
Sì, davvero non capisco perché un programmatore debba sapere come funzionano SRAM e DRAM a livello analogico, il che non aiuta molto durante la scrittura di programmi. E le persone che hanno davvero bisogno di quella conoscenza, meglio passare il tempo a leggere i manuali sui dettagli sui tempi effettivi, ecc. Ma per le persone interessate alle cose di basso livello HW? Forse non utile, ma almeno divertente.
Voo

47
Al giorno d'oggi performance == performance di memoria, quindi comprendere la memoria è la cosa più importante in qualsiasi applicazione ad alte prestazioni. Questo rende il documento essenziale per chiunque sia coinvolto in: sviluppo di giochi, informatica scientifica, finanza, database, compilatori, elaborazione di grandi set di dati, visualizzazione, tutto ciò che deve gestire molte richieste ... Quindi sì, se stai lavorando in un'applicazione che è inattivo la maggior parte delle volte, come un editor di testo, il documento non è completamente interessante fino a quando non devi fare qualcosa in fretta come trovare una parola, contare le parole, controllare l'ortografia ... oh aspetta ... non importa.
gnzlbg,

144

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 ++) in 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 / latencye 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.


(di solito) Non utilizzare il prefetch software

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 addprobabilmente 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_addverrà compilato in un lock add(o lock xaddse viene utilizzato il valore restituito), ma un algoritmo che utilizza CAS per fare qualcosa che non può essere fatto con lockun'istruzione ed di solito non è un disastro. Usa C ++ 11std::atomic o C11 stdatomicinvece dei __syncbuilt-in legacy gcc o dei nuovi __atomicbuilt-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 unionhack: 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/defragkernel 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 perfha sostituito per lo più oprofile. Per eventi dettagliati specifici di determinate microarchitettura, utilizzare il ocperf.pywrapper . 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? .


3
Risposta e suggerimenti molto istruttivi! Questo merita chiaramente più voti!
artiglio

@Peter Cordes Ci sono altre guide / documenti che consiglieresti di leggere? Non sono un programmatore ad alte prestazioni, ma mi piacerebbe saperne di più e spero di acquisire pratiche che posso incorporare nella mia programmazione quotidiana.
user3927312

4
@ user3927312: agner.org/optimize è una delle guide migliori e più coerenti alle cose di basso livello per x86 in particolare, ma alcune delle idee generali si applicano ad altri ISA. Oltre alle guide asm, Agner ha un PDF C ++ ottimizzante. Per altri collegamenti prestazioni / architettura della CPU, consultare stackoverflow.com/tags/x86/info . Ho anche scritto alcune cose sull'ottimizzazione del C ++ aiutando il compilatore a fare meglio le richieste per i cicli critici quando vale la pena dare un'occhiata all'output del compilatore asm: codice C ++ per testare le congetture di Collatz più velocemente di quelle scritte a mano?
Peter Cordes,

74

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.


5
Mi sarebbe piaciuto accettarli entrambi. Ma ho votato il tuo post.
Framester,

5
Probabilmente il cambiamento più importante che è rilevante per gli sviluppatori SW è che i thread di prefetch sono una cattiva idea. Le CPU sono abbastanza potenti per eseguire 2 thread completi con hyperthreading e hanno un prefetch HW molto migliore. Il prefetch SW in generale è molto meno importante, specialmente per l'accesso sequenziale. Vedi la mia risposta
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.