Come si scrive un codice che utilizza al meglio la cache della CPU per migliorare le prestazioni?


159

Potrebbe sembrare una domanda soggettiva, ma quello che sto cercando sono casi specifici, che potresti aver riscontrato in relazione a questo.

  1. Come rendere il codice, cache efficace / cache friendly (più accessi alla cache, il minor numero possibile di errori nella cache)? Da entrambi i punti di vista, cache di dati e cache di programma (cache di istruzioni), vale a dire ciò che le cose nel proprio codice, relative alle strutture di dati e ai costrutti di codice, dovrebbero occuparsi di renderlo efficace.

  2. Esistono strutture dati particolari che è necessario utilizzare / evitare o esiste un modo particolare di accedere ai membri di tale struttura, ecc. Per rendere efficace la cache del codice.

  3. Ci sono costrutti di programma (se, per, switch, break, goto, ...), flusso di codice (per dentro un if, se dentro a per, ecc ...) si dovrebbe seguire / evitare in questa materia?

Non vedo l'ora di ascoltare le singole esperienze relative alla creazione di un codice cache efficiente in generale. Può essere qualsiasi linguaggio di programmazione (C, C ++, Assembly, ...), qualsiasi destinazione hardware (ARM, Intel, PowerPC, ...), qualsiasi sistema operativo (Windows, Linux, S ymbian, ...), ecc. .

La varietà aiuterà a comprenderla meglio.


1
Come introduzione questo discorso offre una buona panoramica di youtu.be/BP6NxVxDQIs
schoetbi

L'URL abbreviato sopra sembra non funzionare più, questo è l'URL completo del discorso: youtube.com/watch?v=BP6NxVxDQIs
Abhinav Upadhyay

Risposte:


119

La cache è lì per ridurre il numero di volte in cui la CPU si fermerà in attesa che una richiesta di memoria venga soddisfatta (evitando la latenza di memoria ) e, come secondo effetto, possibilmente per ridurre la quantità complessiva di dati che devono essere trasferiti (preservando larghezza di banda di memoria ).

Le tecniche per evitare di soffrire di latenza nel recupero della memoria sono in genere la prima cosa da considerare, e talvolta aiutano molto. La larghezza di banda della memoria limitata è anche un fattore limitante, in particolare per applicazioni multicore e multithread in cui molti thread vogliono utilizzare il bus di memoria. Una diversa serie di tecniche aiuta a risolvere quest'ultimo problema.

Migliorare la località spaziale significa assicurarsi che ogni riga della cache sia utilizzata per intero una volta che è stata mappata su una cache. Quando abbiamo esaminato vari benchmark standard, abbiamo visto che una parte sorprendente di questi non riesce a utilizzare il 100% delle righe della cache recuperate prima che le righe della cache vengano sfrattate.

Il miglioramento dell'utilizzo della linea cache aiuta in tre aspetti:

  • Tende a contenere dati più utili nella cache, aumentando sostanzialmente la dimensione effettiva della cache.
  • Tende a contenere dati più utili nella stessa riga della cache, aumentando la probabilità che i dati richiesti possano essere trovati nella cache.
  • Riduce i requisiti di larghezza di banda della memoria, poiché ci saranno meno recuperi.

Le tecniche comuni sono:

  • Usa tipi di dati più piccoli
  • Organizza i tuoi dati per evitare buchi di allineamento (ordinare i membri della struttura diminuendo le dimensioni è un modo)
  • Prestare attenzione all'allocatore di memoria dinamica standard, che può introdurre buchi e diffondere i dati in memoria durante il riscaldamento.
  • Assicurarsi che tutti i dati adiacenti siano effettivamente utilizzati negli hot loop. Altrimenti, prendere in considerazione la suddivisione delle strutture di dati in componenti caldi e freddi, in modo che gli hot loop utilizzino dati caldi.
  • evitare algoritmi e strutture di dati che presentano schemi di accesso irregolari e favorire strutture di dati lineari.

Dobbiamo anche notare che ci sono altri modi per nascondere la latenza della memoria oltre all'utilizzo delle cache.

CPU moderna: spesso hanno uno o più prefetcher hardware . Si allenano sui fallimenti in una cache e cercano di individuare le regolarità. Ad esempio, dopo alcuni errori nelle successive righe della cache, il prefetcher hw inizierà a recuperare le righe della cache nella cache, anticipando le esigenze dell'applicazione. Se hai un modello di accesso regolare, il prefetcher hardware di solito sta facendo un ottimo lavoro. E se il tuo programma non mostra schemi di accesso regolari, puoi migliorare le cose aggiungendo tu stesso le istruzioni di prefetch .

Raggruppando le istruzioni in modo tale che quelle che mancano sempre nella cache siano vicine l'una all'altra, la CPU a volte può sovrapporre questi recuperi in modo che l'applicazione sostenga solo un colpo di latenza ( parallelismo a livello di memoria ).

Per ridurre la pressione complessiva del bus di memoria, è necessario iniziare a indirizzare quella che viene chiamata località temporale . Ciò significa che devi riutilizzare i dati mentre non sono ancora stati sfrattati dalla cache.

Unendo i loop che toccano gli stessi dati ( loop fusion ) e impiegando tecniche di riscrittura note come piastrellatura o blocco, tutti si sforzano di evitare ulteriori recuperi di memoria.

Mentre ci sono alcune regole pratiche per questo esercizio di riscrittura, in genere devi considerare attentamente le dipendenze dei dati trasmesse in loop, per assicurarti di non influenzare la semantica del programma.

Queste cose sono ciò che paga davvero nel mondo multicore, dove in genere non vedrai molti miglioramenti del throughput dopo aver aggiunto il secondo thread.


5
Quando abbiamo esaminato vari benchmark standard, abbiamo visto che una parte sorprendente di questi non riesce a utilizzare il 100% delle righe della cache recuperate prima che le righe della cache vengano sfrattate. Posso chiederti che tipo di strumenti di profilazione ti offre questo tipo di informazioni e come?
Dragon Energy,

"Organizza i tuoi dati per evitare buchi di allineamento (ordinare i membri della struttura diminuendo le dimensioni è un modo)" - perché il compilatore non lo ottimizza da solo? perché il compilatore non può sempre "ordinare i membri diminuendo le dimensioni"? qual è il vantaggio di mantenere i membri non ordinati?
javapowered il

Non conosco le origini, ma per uno, l'ordine dei membri è cruciale nella comunicazione di rete, dove potresti voler inviare intere strutture byte per byte sul web.
Kobrar

1
@javapowered Il compilatore potrebbe essere in grado di farlo a seconda della lingua, anche se non sono sicuro che qualcuno di loro lo faccia. Il motivo per cui non è possibile farlo in C è che è perfettamente valido indirizzare i membri per indirizzo di base + offset anziché per nome, il che significa che il riordino dei membri interromperebbe completamente il programma.
Dan Bechard,

56

Non riesco a credere che non ci siano altre risposte a questo. Ad ogni modo, un classico esempio è quello di iterare un array multidimensionale "dentro e fuori":

pseudocode
for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[j][i]

Il motivo per cui questa cache è inefficiente è perché le moderne CPU caricheranno la riga della cache con indirizzi di memoria "vicini" dalla memoria principale quando si accede a un singolo indirizzo di memoria. Stiamo iterando attraverso le righe "j" (esterne) nell'array nel ciclo interno, quindi per ogni viaggio nel ciclo interno, la linea della cache verrà scaricata e caricata con una linea di indirizzi vicini al [ j] [i] voce. Se questo viene modificato in equivalente:

for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[i][j]

Funzionerà molto più velocemente.


9
al college avevamo un incarico sulla moltiplicazione delle matrici. Si è scoperto che era più veloce prendere una trasposizione della matrice "colonne" per prima e moltiplicare le righe per le righe anziché le righe per i col per quel motivo preciso.
ykaganovich,

11
in realtà, la maggior parte dei compilatori moderni può capirlo da soli (con ottimizzazioni attivate)
Ricardo Nolde,

1
@ykaganovich Questo è anche l'esempio nell'articolo di Ulrich Dreppers: lwn.net/Articles/255364
Simon Stender Boisen

Non sono sicuro che sia sempre corretto - se l'intero array si inserisce nella cache L1 (spesso 32k!) Entrambi gli ordini avranno lo stesso numero di hit e miss della cache. Forse il pre-recupero della memoria potrebbe avere qualche impatto immagino. Felice di essere corretto ovviamente.
Matt Parkins,

chi mai sceglierà la prima versione di questo codice se l'ordine non ha importanza?
silver_rocket

45

Le regole di base sono in realtà abbastanza semplici. Dove diventa difficile è come si applicano al tuo codice.

La cache funziona su due principi: località temporale e località spaziale. La prima è l'idea che se di recente hai usato un certo blocco di dati, probabilmente ne avrai bisogno presto. Quest'ultimo significa che se hai usato di recente i dati all'indirizzo X, probabilmente avrai presto bisogno dell'indirizzo X + 1.

La cache cerca di soddisfare questo problema ricordando i blocchi di dati utilizzati più di recente. Funziona con linee di cache, in genere di dimensioni pari a circa 128 byte, quindi anche se è necessario un solo byte, l'intera riga di cache che lo contiene viene inserita nella cache. Quindi, se in seguito hai bisogno del seguente byte, sarà già nella cache.

E questo significa che vorrai sempre che il tuo codice sfrutti il ​​più possibile queste due forme di località. Non saltare tutta la memoria. Fai più lavoro che puoi su una piccola area, quindi passa a quello successivo e fai più lavoro lì che puoi.

Un semplice esempio è l'attraversamento di array 2D mostrato dalla risposta del 1800. Se lo attraversi una riga alla volta, stai leggendo la memoria in sequenza. Se lo fai in base alla colonna, leggerai una voce, quindi salti in una posizione completamente diversa (l'inizio della riga successiva), leggi una voce e salta di nuovo. E quando finalmente torni alla prima riga, non sarà più nella cache.

Lo stesso vale per il codice. Salti o rami significano un utilizzo della cache meno efficiente (perché non stai leggendo le istruzioni in sequenza, ma stai saltando a un indirizzo diverso). Ovviamente, le piccole istruzioni if ​​probabilmente non cambieranno nulla (stai saltando solo pochi byte, quindi finirai ancora all'interno della regione cache), ma le chiamate di funzione in genere implicano che stai saltando in un modo completamente diverso indirizzo che non può essere memorizzato nella cache. A meno che non sia stato chiamato di recente.

L'utilizzo della cache delle istruzioni di solito è tuttavia molto meno problematico. Di solito è necessario preoccuparsi della cache dei dati.

In una struttura o classe, tutti i membri sono disposti in modo contiguo, il che è positivo. In un array, anche tutte le voci sono disposte in modo contiguo. Negli elenchi collegati, ogni nodo è allocato in una posizione completamente diversa, il che è negativo. I puntatori in generale tendono a puntare a indirizzi non correlati, il che probabilmente causerà un errore nella cache se lo si differenzia.

E se vuoi sfruttare più core, può diventare davvero interessante, come al solito, solo una CPU può avere un dato indirizzo nella sua cache L1 alla volta. Quindi, se entrambi i core accedono costantemente allo stesso indirizzo, si tradurranno in costanti errori nella cache, poiché combattono per l'indirizzo.


4
+1, consigli buoni e pratici. Un'aggiunta: la località temporale e la località spaziale suggeriscono che, ad esempio per le operazioni a matrice, potrebbe essere consigliabile dividerle in matrici più piccole che si adattano completamente in una riga della cache o le cui righe / colonne si adattano alle righe della cache. Ricordo di averlo fatto per la visualizzazione di multidim. dati. Ha fornito un calcio serio nei pantaloni. È bene ricordare che la cache contiene più di una 'linea';)
AndreasT

1
Dici che solo 1 CPU può avere un determinato indirizzo alla volta nella cache L1 - suppongo che intendi linee di cache piuttosto che indirizzo. Ho anche sentito parlare di falsi problemi di condivisione quando almeno una delle CPU sta scrivendo, ma non se entrambe stanno solo facendo letture. Quindi per "accesso" intendi effettivamente le scritture?
Joseph Garvin,

2
@JosephGarvin: sì, intendevo scrivere. Hai ragione, più core possono avere le stesse linee di cache nelle loro cache L1 allo stesso tempo, ma quando un core scrive su questi indirizzi, viene invalidato in tutte le altre cache L1 e quindi devono ricaricarlo prima di poter fare niente con esso. Ci scusiamo per il testo impreciso (sbagliato). :)
jalf

44

Consiglio di leggere l'articolo in 9 parti Che cosa ogni programmatore dovrebbe sapere sulla memoria di Ulrich Drepper se sei interessato a come la memoria e il software interagiscono. È disponibile anche come PDF da 104 pagine .

Le sezioni particolarmente rilevanti per questa domanda potrebbero essere la Parte 2 (cache della CPU) e la Parte 5 (Cosa possono fare i programmatori: ottimizzazione della cache).


16
È necessario aggiungere un riepilogo dei punti principali dell'articolo.
Azmisov,

Ottima lettura, ma un altro libro che DEVE essere menzionato qui è Hennessy, Patterson, Computer Architecture, A Quantitiative Approach , che è oggi disponibile alla sua quinta edizione.
Haymo Kutschbach,

15

Oltre ai modelli di accesso ai dati, un fattore importante nel codice compatibile con la cache è la dimensione dei dati . Meno dati significa che più si adatta alla cache.

Questo è principalmente un fattore con strutture di dati allineate alla memoria. La saggezza "convenzionale" afferma che le strutture di dati devono essere allineate ai confini delle parole perché la CPU può accedere solo a parole intere e se una parola contiene più di un valore, è necessario svolgere un lavoro extra (lettura-modifica-scrittura anziché una semplice scrittura) . Ma le cache possono invalidare completamente questo argomento.

Allo stesso modo, un array booleano Java utilizza un intero byte per ciascun valore per consentire di operare direttamente su singoli valori. È possibile ridurre la dimensione dei dati di un fattore 8 se si utilizzano bit effettivi, ma l'accesso ai singoli valori diventa molto più complesso, richiedendo operazioni di spostamento dei bit e maschera (la BitSetclasse fa questo per te). Tuttavia, a causa degli effetti della cache, questo può essere considerevolmente più veloce rispetto all'utilizzo di un valore booleano [] quando l'array è grande. IIRC una volta ho raggiunto uno speedup di un fattore 2 o 3 in questo modo.


9

La struttura di dati più efficace per una cache è un array. Le cache funzionano meglio, se la struttura dei dati è strutturata in modo sequenziale quando le CPU leggono intere righe della cache (in genere 32 byte o più) contemporaneamente dalla memoria principale.

Qualsiasi algoritmo che accede alla memoria in ordine casuale elimina le cache perché ha sempre bisogno di nuove righe della cache per adattarsi alla memoria a cui si accede in modo casuale. D'altra parte è meglio un algoritmo, che scorre in sequenza attraverso un array perché:

  1. Dà alla CPU la possibilità di leggere più avanti, ad esempio inserendo speculativamente più memoria nella cache, a cui si accederà in seguito. Questo read-ahead offre un enorme incremento delle prestazioni.

  2. L'esecuzione di un loop stretto su un array di grandi dimensioni consente inoltre alla CPU di memorizzare nella cache l'esecuzione del codice nel loop e nella maggior parte dei casi consente di eseguire un algoritmo interamente dalla memoria cache senza dover bloccare l'accesso alla memoria esterna.


@Grover: A proposito del tuo punto 2. quindi si può dire che se insidea di un ciclo stretto, viene chiamata una funzione per ogni conteggio dei cicli, allora recupererà del tutto il nuovo codice e causerà una mancanza della cache, invece se puoi mettere la funzione come un codice nel ciclo for stesso, nessuna chiamata di funzione, sarebbe più veloce a causa di un minor numero di errori nella cache?
goldenmean,

1
Sì e no. La nuova funzione verrà caricata nella cache. Se lo spazio nella cache è sufficiente, alla seconda iterazione avrà già quella funzione nella cache, quindi non c'è motivo di ricaricarlo di nuovo. Quindi è un successo alla prima chiamata. In C / C ++ puoi chiedere al compilatore di posizionare le funzioni una accanto all'altra usando i segmenti appropriati.
Grover,

Un'altra nota: se si chiama fuori dal ciclo e non c'è abbastanza spazio nella cache, la nuova funzione verrà caricata nella cache indipendentemente. Può anche succedere che il ciclo originale venga espulso dalla cache. In questo caso la chiamata comporterà fino a tre penalità per ogni iterazione: una per caricare il target della chiamata e un'altra per ricaricare il loop. E un terzo se l'head del loop non si trova nella stessa riga della cache dell'indirizzo di ritorno della chiamata. In tal caso, saltare alla testa del loop richiede anche un nuovo accesso alla memoria.
Grover,

8

Un esempio che ho visto usato in un motore di gioco è stato quello di spostare i dati fuori dagli oggetti e nei loro array. Un oggetto di gioco soggetto alla fisica potrebbe avere anche molti altri dati ad esso collegati. Ma durante il ciclo di aggiornamento della fisica tutto il motore a cui importava erano i dati relativi a posizione, velocità, massa, riquadro di limitazione, ecc. Quindi tutto ciò veniva inserito nei propri array e ottimizzato il più possibile per SSE.

Quindi durante il ciclo della fisica i dati della fisica sono stati elaborati in ordine di array usando la matematica vettoriale. Gli oggetti di gioco hanno usato il loro ID oggetto come indice nei vari array. Non era un puntatore perché i puntatori potevano essere invalidati se gli array dovessero essere trasferiti.

In molti modi questo ha violato i modelli di progettazione orientati agli oggetti, ma ha reso il codice molto più veloce mettendo i dati vicini tra loro che dovevano essere operati negli stessi loop.

Questo esempio è probabilmente obsoleto perché mi aspetto che la maggior parte dei giochi moderni utilizzi un motore fisico precompilato come Havok.


2
+1 Per niente obsoleto. Questo è il modo migliore per organizzare i dati per i motori di gioco: rendere contigui i blocchi di dati ed eseguire tutte le operazioni di un determinato tipo di operazione (ad esempio AI) prima di passare al successivo (ad esempio fisica) al fine di sfruttare la prossimità / località della cache di riferimento.
Ingegnere

Ho visto questo esempio esatto in un video da qualche parte un paio di settimane fa, ma da allora ho perso il link ad esso / non ricordo come trovarlo. Ricordi dove hai visto questo esempio?
saranno

@will: No, non ricordo esattamente dove fosse.
Zan Lynx,

Questa è l'idea stessa di un sistema di componenti di entità (ECS: en.wikipedia.org/wiki/Entity_component_system ). Archiviare i dati come array di struttura anziché come array di strutture più tradizionali incoraggiati dalle pratiche OOP.
BuschnicK,

7

È stato toccato solo un post, ma emerge un grosso problema quando si condividono dati tra processi. Si desidera evitare che più processi tentino di modificare contemporaneamente la stessa riga della cache. Qualcosa a cui prestare attenzione qui è la condivisione "falsa", in cui due strutture dati adiacenti condividono una linea cache e le modifiche a una invalida la linea cache per l'altra. Ciò può causare inutilmente lo spostamento avanti e indietro delle righe della cache tra le cache del processore che condividono i dati su un sistema multiprocessore. Un modo per evitarlo è allineare e riempire le strutture di dati per metterle su linee diverse.


7

Un'osservazione al "classico esempio" da parte dell'utente 1800 INFORMAZIONI (troppo tempo per un commento)

Volevo verificare le differenze di tempo per due ordini di iterazione ("esterno" e "interno"), quindi ho fatto un semplice esperimento con un grande array 2D:

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

e il secondo caso con i forloop scambiati.

La versione più lenta ("x prima") era 0,88 secondi e quella più veloce, 0,06 secondi. Questo è il potere della memorizzazione nella cache :)

Ho usato gcc -O2e ancora i loop non sono stati ottimizzati. Il commento di Ricardo secondo cui "la maggior parte dei compilatori moderni può capirlo da soli" non regge


Non sono sicuro di averlo capito. In entrambi gli esempi stai ancora accedendo a ciascuna variabile nel ciclo for. Perché un modo è più veloce dell'altro?
ed-

in definitiva intuitivo per me capire come influenza :)
Laie

@EdwardCorlew È a causa dell'ordine in cui sono accessibili. Il primo ordine y è più veloce perché accede ai dati in sequenza. Quando viene richiesta la prima voce, la cache L1 carica un'intera riga della cache, che include l'int richiesto più i successivi 15 (supponendo una riga della cache a 64 byte), quindi non vi è alcuno stallo della CPU in attesa dei successivi 15. La x il primo ordine è più lento perché l'elemento a cui si accede non è sequenziale e presumibilmente N è abbastanza grande da consentire alla memoria di accedere sempre all'esterno della cache L1 e quindi ogni operazione si blocca.
Matt Parkins,

4

Posso rispondere (2) dicendo che nel mondo C ++, gli elenchi collegati possono facilmente uccidere la cache della CPU. Le matrici sono una soluzione migliore ove possibile. Nessuna esperienza sul fatto che lo stesso valga per altre lingue, ma è facile immaginare che sorgano gli stessi problemi.


@Andrew: che ne dici delle strutture. Sono efficienti nella cache? Hanno dei limiti di dimensione per essere efficienti nella cache?
goldenmean,

Una struttura è un singolo blocco di memoria, quindi finché non supera la dimensione della cache non vedrai alcun impatto. È solo quando hai una raccolta di strutture (o classi) che vedrai gli hit della cache e dipende dal modo in cui organizzi la raccolta. Un array unisce gli oggetti uno contro l'altro (buono) ma un elenco collegato può avere oggetti in tutto lo spazio degli indirizzi con collegamenti tra loro, il che è ovviamente negativo per le prestazioni della cache.
Andrew,

Un modo per utilizzare gli elenchi collegati senza uccidere la cache, più efficace per elenchi non grandi, è quello di creare il proprio pool di memoria, ovvero di allocare un array di grandi dimensioni. quindi invece di 'malloc'ing (o' new'ing in C ++) memoria per ogni piccolo membro dell'elenco collegato, che può essere allocato in una posizione completamente diversa nella memoria, e sprecando spazio di gestione, gli dai la memoria dal tuo pool di memoria, aumentando notevolmente le probabilità che chiudono logicamente i membri dell'elenco, saranno nella cache insieme.
Liran Orevi,

Certo, ma è un sacco di lavoro per ottenere std :: list <> et al. per usare i tuoi blocchi di memoria personalizzati. Quando ero un giovane tiratore di frusta avrei assolutamente seguito quella strada, ma in questi giorni ... troppe altre cose da affrontare.
Andrew,


4

La cache è organizzata in "linee di cache" e la memoria (reale) viene letta e scritta in blocchi di queste dimensioni.

Le strutture di dati contenute in una singola riga della cache sono quindi più efficienti.

Allo stesso modo, gli algoritmi che accedono a blocchi di memoria contigui saranno più efficienti degli algoritmi che saltano attraverso la memoria in un ordine casuale.

Sfortunatamente la dimensione della linea della cache varia notevolmente tra i processori, quindi non c'è modo di garantire che una struttura di dati che sia ottimale su un processore sia efficiente su un altro.


non necessariamente. fai solo attenzione alla falsa condivisione. a volte è necessario dividere i dati in diverse righe della cache. quanto è efficace la cache si basa sempre su come la usi.
DAG,

4

Chiedere come rendere un codice, cache friendly-cache friendly e la maggior parte delle altre domande, di solito è chiedere come ottimizzare un programma, perché la cache ha un impatto così grande sulle prestazioni che qualsiasi programma ottimizzato è uno che è cache cache efficace.

Suggerisco di leggere sull'ottimizzazione, ci sono alcune buone risposte su questo sito. In termini di libri, raccomando su Computer Systems: A Programmer's Perspective, che contiene alcuni ottimi testi sull'uso corretto della cache.

(a proposito - per quanto possa essere una mancanza di cache, c'è di peggio - se un programma esegue il paging dal disco rigido ...)


4

Ci sono state molte risposte su consigli generali come la selezione della struttura dei dati, il modello di accesso, ecc. Qui vorrei aggiungere un altro modello di progettazione del codice chiamato pipeline del software che utilizza la gestione attiva della cache.

L'idea è presa in prestito da altre tecniche di pipeline, ad esempio pipeline di istruzioni CPU.

Questo tipo di modello si applica meglio alle procedure che

  1. potrebbe essere suddiviso in più passaggi secondari ragionevoli, S [1], S [2], S [3], ... il cui tempo di esecuzione è approssimativamente paragonabile al tempo di accesso alla RAM (~ 60-70ns).
  2. prende un batch di input e fa su di essi più passaggi sopra menzionati per ottenere risultati.

Prendiamo un semplice caso in cui esiste una sola procedura secondaria. Normalmente il codice vorrebbe:

def proc(input):
    return sub-step(input))

Per prestazioni migliori, è possibile passare più input alla funzione in un batch in modo da ammortizzare l'overhead della chiamata di funzione e aumentare anche la localizzazione della cache del codice.

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

Tuttavia, come detto in precedenza, se l'esecuzione del passaggio è all'incirca la stessa del tempo di accesso alla RAM, è possibile migliorare ulteriormente il codice in questo modo:

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

Il flusso di esecuzione sarebbe simile a:

  1. prefetch (1) chiede alla CPU di precaricare l'input [1] nella cache, dove l'istruzione prefetch prende P cicli da sola e ritorna, e in background l'input [1] arriva nella cache dopo i cicli R.
  2. works_on (0) cold miss on 0 e ci lavora sopra, il che richiede M
  3. prefetch (2) emette un altro fetch
  4. works_on (1) se P + R <= M, quindi gli input [1] dovrebbero essere nella cache già prima di questo passaggio, quindi evitare un errore nella cache dei dati
  5. works_on (2) ...

Potrebbero essere necessari più passaggi, quindi è possibile progettare una pipeline in più fasi fintanto che la tempistica dei passaggi e la latenza di accesso alla memoria corrispondano, si verificherebbe una piccola perdita della cache di codice / dati. Tuttavia, questo processo deve essere sintonizzato con molti esperimenti per scoprire il giusto raggruppamento di passaggi e il tempo di prefetch. Grazie allo sforzo richiesto, vede una maggiore adozione nell'elaborazione del flusso di pacchetti / dati ad alte prestazioni. Un buon esempio di codice di produzione può essere trovato nella progettazione della pipeline DPDK QoS Enqueue: http://dpdk.org/doc/guides/prog_guide/qos_framework.html Capitolo 21.2.4.3. Enqueue Pipeline.

Ulteriori informazioni possono essere trovate:

https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

Scrivi il tuo programma per prendere una dimensione minima. Ecco perché non è sempre una buona idea utilizzare le ottimizzazioni -O3 per GCC. Occupa una dimensione maggiore. Spesso -Os è buono quanto -O2. Tutto dipende però dal processore utilizzato. YMMV.

Lavora con piccoli blocchi di dati alla volta. Ecco perché un algoritmo di ordinamento meno efficiente può essere eseguito più rapidamente di quicksort se il set di dati è di grandi dimensioni. Trova i modi per suddividere i tuoi set di dati più grandi in quelli più piccoli. Altri hanno suggerito questo.

Per aiutarti a sfruttare meglio la località temporale / spaziale delle istruzioni, potresti voler studiare come il tuo codice viene convertito in assembly. Per esempio:

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

I due loop producono codici diversi anche se stanno semplicemente analizzando un array. In ogni caso, la tua domanda è molto specifica per l'architettura. Pertanto, l'unico modo per controllare strettamente l'utilizzo della cache è comprendere come funziona l'hardware e ottimizzare il codice per esso.


Punto interessante Le cache look-ahead fanno ipotesi basate sulla direzione di un loop / passaggio attraverso la memoria?
Andrew,

1
Esistono molti modi per progettare cache di dati speculativi. Quelli basati sul passo misurano la "distanza" e la "direzione" degli accessi ai dati. Quelli basati sul contenuto inseguono catene di puntatori. Esistono altri modi per progettarli.
sybreon,

1

Oltre ad allineare la struttura e i campi, se la struttura in caso di heap allocato è possibile utilizzare allocatori che supportano allocazioni allineate; come _aligned_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE); altrimenti potresti avere una falsa condivisione casuale; ricorda che in Windows, l'heap predefinito ha un allineamento di 16 byte.

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.