Come funzionano le linee della cache?


169

Comprendo che il processore porta i dati nella cache tramite le linee della cache, che - per esempio, sul mio processore Atom - porta circa 64 byte alla volta, qualunque sia la dimensione dei dati reali letti.

La mia domanda è:

Immagina di dover leggere un byte dalla memoria, quali 64 byte verranno portati nella cache?

Le due possibilità che posso vedere è che, o i 64 byte iniziano dal limite di 64 byte più vicino al di sotto del byte di interesse, oppure i 64 byte sono distribuiti attorno al byte in un modo predeterminato (ad esempio, metà sotto, metà sopra o tutto sopra).

Cos'è questo?


22
Leggi questo: ciò che ogni programmatore dovrebbe sapere sulla memoria . Quindi rileggilo. Fonte migliore (pdf) qui .
andersoj,

Risposte:


129

Se la riga della cache contenente il byte o la parola che stai caricando non è già presente nella cache, la tua CPU richiederà i 64 byte che iniziano al limite della riga della cache (l'indirizzo più grande sotto quello di cui hai bisogno è multiplo di 64) .

I moderni moduli di memoria per PC trasferiscono 64 bit (8 byte) alla volta, in una sequenza di otto trasferimenti , quindi un comando avvia la lettura o la scrittura di una linea cache completa dalla memoria. (La dimensione del trasferimento burst della SDRAM DDR1 / 2/3/4 è configurabile fino a 64B; le CPU selezioneranno la dimensione del trasferimento burst in modo che corrisponda alla dimensione della loro linea di cache, ma 64B è comune)

Come regola generale, se il processore non è in grado di prevedere un accesso alla memoria (e di prenderlo in considerazione), il processo di recupero può richiedere ~ 90 nanosecondi o ~ 250 cicli di clock (dalla CPU che conosce l'indirizzo alla CPU che riceve i dati).

Al contrario, un hit nella cache L1 ha una latenza di caricamento di 3 o 4 cicli e un ricaricamento del negozio ha una latenza di inoltro del negozio di 4 o 5 cicli su moderne CPU x86. Le cose sono simili su altre architetture.

Ulteriori letture: quello che ogni programmatore dovrebbe sapere sulla memoria di Ulrich Drepper . Il consiglio di prefetch del software è un po 'obsoleto: i prefetcher HW moderni sono più intelligenti e l'hyperthreading è molto meglio rispetto ai giorni P4 (quindi un thread di prefetch è in genere uno spreco). Anche il tag wiki ha molti collegamenti prestazionali per quell'architettura.


1
Questa risposta non ha assolutamente senso. Cosa c'entra la larghezza di banda della memoria a 64 bit (che è anche errata a tale riguardo) con il 64 byte (!) Che non è un bit da fare? Anche i 10-30 ns sono totalmente sbagliati se colpisci il Ram. Potrebbe essere vero per la cache L3 o L2 ma non per la RAM in cui è più simile a 90ns. Quello che vuoi dire è il tempo di scoppio - il tempo di accedere alla prossima quad-word in modalità burst (che in realtà è la risposta corretta)
Martin Kersten

5
@MartinKersten: un canale di DDR1 / 2/3/4 SDRAM utilizza una larghezza del bus dati a 64 bit. Un trasferimento a raffica di un'intera riga della cache richiede otto trasferimenti di 8B ciascuno ed è ciò che effettivamente accade. Potrebbe essere ancora corretto che il processo sia ottimizzato trasferendo prima il blocco allineato a 8B contenente prima il byte desiderato, ovvero iniziando il burst lì (e avvolgendosi se non fosse il primo 8B della dimensione del trasferimento burst). Le moderne CPU con cache multi-livello probabilmente non lo fanno più, perché significherebbe inoltrare i primi blocchi della burst fino alla cache L1 in anticipo.
Peter Cordes,

2
Haswell ha un percorso 64B tra la cache L2 e L1D (ovvero una larghezza della linea della cache completa), quindi il trasferimento dell'8B contenente il byte richiesto comporterebbe un uso inefficiente di quel bus. @Martin ha anche ragione sul tempo di accesso per un carico che deve andare nella memoria principale.
Peter Cordes,

3
Buona domanda se i dati salgono immediatamente nella gerarchia di memoria o se L3 attende una riga intera dalla memoria prima di iniziare a inviarlo a L2. Esistono buffer di trasferimento tra diversi livelli di cache e ogni miss in sospeso ne richiede uno. Quindi ( congetture totali ) probabilmente L3 inserisce i byte dal controller di memoria nel proprio buffer di ricezione contemporaneamente mettendoli nel buffer di caricamento appropriato per la cache L2 che lo desiderava. Quando la linea viene trasferita completamente dalla memoria, L3 notifica a L2 che la linea è pronta e la copia nel proprio array.
Peter Cordes,

2
@Martin: ho deciso di andare avanti e modificare questa risposta. Penso che sia più preciso ora e ancora semplice. Lettori futuri: vedi anche la domanda di Mike76 e la mia risposta: stackoverflow.com/questions/39182060/…
Peter Cordes,

22

Se le righe della cache hanno una larghezza di 64 byte, corrispondono a blocchi di memoria che iniziano su indirizzi divisibili per 64. I 6 bit meno significativi di qualsiasi indirizzo sono un offset nella riga della cache.

Quindi per ogni dato byte, la riga della cache che deve essere recuperata può essere trovata cancellando i sei bit meno significativi dell'indirizzo, che corrisponde all'arrotondamento per l'indirizzo più vicino che è divisibile per 64.

Sebbene ciò sia fatto dall'hardware, possiamo mostrare i calcoli usando alcune definizioni di macro C di riferimento:

#define CACHE_BLOCK_BITS 6
#define CACHE_BLOCK_SIZE (1U << CACHE_BLOCK_BITS)  /* 64 */
#define CACHE_BLOCK_MASK (CACHE_BLOCK_SIZE - 1)    /* 63, 0x3F */

/* Which byte offset in its cache block does this address reference? */
#define CACHE_BLOCK_OFFSET(ADDR) ((ADDR) & CACHE_BLOCK_MASK)

/* Address of 64 byte block brought into the cache when ADDR accessed */
#define CACHE_BLOCK_ALIGNED_ADDR(ADDR) ((ADDR) & ~CACHE_BLOCK_MASK)

1
Ho difficoltà a capirlo. So che è di 2 anni dopo, ma puoi darmi un esempio di codice per questo? una o due righe.
Nick,

1
@Nick Il motivo per cui questo metodo funziona risiede nel sistema di numeri binari. Qualsiasi potenza di 2 ha solo un bit impostato e tutti i bit rimanenti cancellati, quindi per 64, 0b1000000noterai che le ultime 6 cifre sono zeri, quindi anche quando hai un numero con uno di quei 6 set (che rappresentano il numero % 64), cancellandoli verrà visualizzato l'indirizzo di memoria allineato a 64 byte più vicino.
legends2k,

21

Innanzitutto l'accesso alla memoria principale è molto costoso. Attualmente una CPU da 2 GHz (la più lenta una volta) ha tick 2G (cicli) al secondo. Una CPU (core virtuale al giorno d'oggi) può recuperare un valore dai suoi registri una volta per tick. Poiché un core virtuale è composto da più unità di elaborazione (ALU - unità logica aritmetica, FPU ecc.), Se possibile, può effettivamente elaborare determinate istruzioni in parallelo.

Un accesso alla memoria principale costa da circa 70ns a 100ns (DDR4 è leggermente più veloce). Questa volta cerca sostanzialmente la cache L1, L2 e L3 e poi tocca la memoria (invia il comando al controller di memoria, che la invia ai banchi di memoria), aspetta la risposta ed è fatta.

100ns significa circa 200 tick. Quindi, in sostanza, se a un programma mancassero sempre le cache a cui accede ogni memoria, la CPU impiegherebbe circa il 99,5% del suo tempo (se legge solo la memoria) in attesa della memoria.

Per velocizzare le cose ci sono le cache L1, L2, L3. Usano la memoria posizionata direttamente sul chip e usando un diverso tipo di circuiti a transistor per memorizzare i bit dati. Ciò richiede più spazio, più energia ed è più costoso della memoria principale poiché una CPU viene generalmente prodotta utilizzando una tecnologia più avanzata e un errore di produzione nella memoria L1, L2, L3 ha la possibilità di rendere la CPU senza valore (difetto), quindi cache di grandi dimensioni L1, L2, L3 aumentano il tasso di errore che diminuisce il rendimento che diminuisce direttamente il ROI. Quindi c'è un enorme compromesso quando si tratta della dimensione della cache disponibile.

(attualmente si creano più cache L1, L2, L3 per essere in grado di disattivare determinate porzioni per ridurre la possibilità che un vero difetto di produzione sia le aree di memoria cache che rendono il difetto della CPU nel suo insieme).

Per dare un'idea di tempismo (fonte: costi per accedere a cache e memoria )

  • Cache L1: da 1ns a 2ns (2-4 cicli)
  • Cache L2: da 3ns a 5ns (6-10 cicli)
  • Cache L3: da 12ns a 20ns (24-40 cicli)
  • RAM: 60ns (120 cicli)

Dato che mescoliamo diversi tipi di CPU, queste sono solo stime, ma danno una buona idea di cosa sta realmente accadendo quando viene recuperato un valore di memoria e potremmo avere un hit o un miss in un determinato livello di cache.

Quindi una cache velocizza notevolmente l'accesso alla memoria (60 ns contro 1 ns).

Recuperare un valore, memorizzarlo nella cache per la possibilità di rileggerlo è utile per le variabili a cui si accede spesso, ma per le operazioni di copia della memoria sarebbe ancora lento poiché si legge un valore, si scrive da qualche parte e non si legge mai il valore di nuovo ... nessun hit nella cache, dead slow (oltre a questo può accadere in parallelo poiché abbiamo un'esecuzione fuori servizio).

Questa copia di memoria è così importante che esistono diversi modi per accelerarla. All'inizio la memoria era spesso in grado di copiare la memoria al di fuori della CPU. È stato gestito direttamente dal controller di memoria, quindi un'operazione di copia della memoria non ha inquinato le cache.

Ma oltre a una semplice copia di memoria, era abbastanza comune l'accesso seriale alla memoria. Un esempio è l'analisi di una serie di informazioni. Avere un array di numeri interi e calcolare la somma, la media, la media o ancora più semplice trovare un certo valore (filtro / ricerca) erano un'altra classe molto importante di algoritmi eseguiti ogni volta su qualsiasi CPU per scopi generici.

Quindi, analizzando il modello di accesso alla memoria era evidente che i dati venivano letti in sequenza molto spesso. C'era un'alta probabilità che se un programma legge il valore all'indice i, anche il programma leggerà il valore i + 1. Questa probabilità è leggermente superiore alla probabilità che lo stesso programma legga anche il valore i + 2 e così via.

Quindi, dato un indirizzo di memoria, era (ed è ancora) una buona idea leggere in anticipo e recuperare valori aggiuntivi. Questo è il motivo per cui esiste una modalità boost.

L'accesso alla memoria in modalità boost significa che viene inviato un indirizzo e vengono inviati in sequenza più valori. Ogni invio di valore aggiuntivo richiede solo circa 10 ns (o anche sotto).

Un altro problema era un indirizzo. L'invio di un indirizzo richiede tempo. Per indirizzare gran parte della memoria, è necessario inviare indirizzi di grandi dimensioni. All'inizio significava che il bus dell'indirizzo non era abbastanza grande per inviare l'indirizzo in un singolo ciclo (tick) e che era necessario più di un ciclo per inviare l'indirizzo aggiungendo più ritardo.

Una riga della cache di 64 byte, ad esempio, significa che la memoria è divisa in blocchi distinti (non sovrapposti) di memoria di 64 byte di dimensione. 64 byte indicano che l'indirizzo iniziale di ciascun blocco ha i sei bit di indirizzo più bassi da essere sempre zeri. Pertanto, non è necessario inviare questi sei bit zero ogni volta per aumentare lo spazio degli indirizzi 64 volte per qualsiasi numero di larghezza del bus dell'indirizzo (effetto di benvenuto).

Un altro problema che la riga della cache risolve (oltre a leggere in anticipo e salvare / liberare sei bit sul bus degli indirizzi) è il modo in cui è organizzata la cache. Ad esempio, se una cache sarebbe divisa in blocchi (celle) da 8 byte (64 bit), è necessario memorizzare l'indirizzo della cella di memoria per cui questa cella cache contiene il valore. Se l'indirizzo sarebbe anche a 64 bit, ciò significa che la metà della dimensione della cache viene consumata dall'indirizzo con un sovraccarico del 100%.

Poiché una riga della cache è di 64 byte e una CPU potrebbe utilizzare 64 bit - 6 bit = 58 bit (non è necessario memorizzare i bit zero troppo a destra) significa che possiamo memorizzare nella cache 64 byte o 512 bit con un sovraccarico di 58 bit (sovraccarico dell'11%). In realtà gli indirizzi memorizzati sono anche più piccoli di questo, ma ci sono informazioni sullo stato (come la linea della cache è valida e accurata, sporca e deve essere riscritta in ram ecc.).

Un altro aspetto è che abbiamo una cache set associativa. Non tutte le celle della cache sono in grado di memorizzare un determinato indirizzo, ma solo un sottoinsieme di quelli. Ciò rende ancora più piccoli i bit di indirizzo memorizzati necessari, consente l'accesso parallelo alla cache (è possibile accedere a ciascun sottoinsieme una volta ma indipendentemente dagli altri sottoinsiemi).

Soprattutto quando si tratta di sincronizzare l'accesso alla cache / memoria tra i diversi core virtuali, le loro unità di elaborazione multiple indipendenti per core e infine i processori multipli su una scheda madre (che contiene schede che ospitano fino a 48 processori e oltre).

Questa è fondamentalmente l'idea attuale del perché abbiamo linee di cache. Il vantaggio di leggere in anticipo è molto elevato e il caso peggiore di leggere un singolo byte da una riga della cache e non rileggere mai più il resto è molto scarso poiché la probabilità è molto ridotta.

La dimensione della cache-line (64) è un saggio compromesso scelto tra le cache-line più grandi rende improbabile che l'ultimo byte venga letto anche nel prossimo futuro, la durata necessaria per recuperare l'intera riga della cache dalla memoria (e per riscriverlo) e anche il sovraccarico nell'organizzazione della cache e la parallelizzazione dell'accesso alla cache e alla memoria.


1
Una cache set-associativa utilizza alcuni bit di indirizzo per selezionare un set, quindi i tag possono essere anche più brevi del tuo esempio. Naturalmente, la cache deve anche tenere traccia di quale tag va con quale array di dati nel set, ma di solito ci sono più set che modi all'interno di un set. (ad es. cache L1D associativa a 8 vie da 32 kB, con 64 linee, nelle CPU Intel x86: offset 6 bit, indice 6 bit. I tag devono essere larghi solo 48-12 bit, perché x86-64 (per ora) ha solo 48- indirizzi fisici di bit. Come sono sicuro che sai, non è una coincidenza che i 12 bit bassi siano l'offset della pagina, quindi L1 può essere VIPT senza aliasing.)
Peter Cordes

sorprendente risposta bud ... c'è un pulsante "mi piace" da qualche parte?
Edgard Lima,

@EdgardLima, non il pulsante di votazione?
Pacerier,

6

I processori possono avere cache multilivello (L1, L2, L3) e queste differiscono per dimensioni e velocità.

Tuttavia, per capire cosa succede esattamente in ogni cache, dovrai studiare il predittore di ramo utilizzato da quel processore specifico e come le istruzioni / i dati del tuo programma si comportano contro di esso.

Informazioni su predittore di filiali , cache della CPU e criteri di sostituzione .

Questo non è un compito facile. Se alla fine della giornata tutto ciò che desideri è un test delle prestazioni, puoi utilizzare uno strumento come Cachegrind . Tuttavia, poiché si tratta di una simulazione, il suo risultato può differire in una certa misura.


4

Non posso dire con certezza poiché ogni hardware è diverso, ma in genere è "64 byte iniziano con il limite di 64 byte più vicino in basso" in quanto si tratta di un'operazione molto veloce e semplice per la CPU.


2
Io posso dire per certo. Qualsiasi progetto di cache ragionevole avrà linee con dimensioni che hanno una potenza di 2 e che sono naturalmente allineate. (ad es. 64B allineato). Non è solo veloce e semplice, è letteralmente gratuito: semplicemente ignori i 6 bit bassi dell'indirizzo, ad esempio. Le cache fanno spesso cose diverse con diversi intervalli di indirizzi. (ad esempio, la cache si preoccupa del tag e dell'indice per rilevare hit vs miss, quindi utilizzare solo l'offset all'interno di una riga della cache per inserire / estrarre dati)
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.