2009-04-02 28 views
5

Ho un'applicazione che trasmette 250 MB di dati, applicando una funzione di soglia neurale semplice e veloce ai blocchi di dati (che sono solo 2 parole di 32 bit ciascuno). In base al risultato del calcolo (molto semplice), il blocco viene imprevedibilmente inserito in uno dei 64 contenitori. Quindi è un grande stream in e 64 stream più brevi (di lunghezza variabile) fuori.Uso efficiente della larghezza di banda di memoria per lo streaming

Questo viene ripetuto molte volte con diverse funzioni di rilevamento.

Il calcolo è limitato dalla larghezza di banda della memoria. Posso dire questo perché non ci sono cambiamenti di velocità, anche se uso una funzione discriminante che è molto più intensiva dal punto di vista computazionale.

Qual è il modo migliore per strutturare le scritture dei nuovi stream per ottimizzare la larghezza di banda della mia memoria? Penso soprattutto che la comprensione dell'uso della cache e della dimensione della linea della cache possa giocare un ruolo importante in questo. Immagina il caso peggiore in cui ho i miei 64 flussi in uscita e, sfortunatamente, molti mappano sulla stessa linea della cache. Quindi, quando scrivo i successivi 64 bit di dati in un flusso, la CPU deve svuotare una riga di cache non aggiornata nella memoria principale e caricare nella riga della cache corretta. Ognuno di questi utilizza 64 BYTES di larghezza di banda ... quindi l'applicazione limitata della mia larghezza di banda potrebbe sprecare il 95% della larghezza di banda della memoria (in questo ipotetico caso peggiore, però).

È difficile persino provare a misurare l'effetto, quindi progettare i modi per aggirarlo è ancora più vago. O sto addirittura inseguendo un collo di bottiglia fantasma che in qualche modo l'hardware si ottimizza meglio di quanto potrei?

Sto utilizzando processori Core II x86 se questo fa alcuna differenza.

Modifica: ecco alcuni esempi di codice. Passa attraverso una matrice e copia i suoi elementi su vari array di output scelti in modo pseudo-casuale. L'esecuzione lo stesso programma con un diverso numero di bidoni di destinazione dà diversi tempi di esecuzione, anche se la stessa quantità di calcolo e di memoria di lettura e scrittura sono stati fatti:

2 flussi in uscita: 13 secondi
8 flussi di uscita: 13 secondi
32 flussi di uscita: 19 sec
128 flussi di uscita: 29 secondi
512 flussi di uscita: 47 secondi

La differenza tra l'utilizzo 512 contro 2 flussi in uscita è 4X, (probabilmente ??) causata da linea di cache sgombero aerea.

#include <stdio.h> 
#include <stdlib.h> 
#include <ctime> 

int main() 
{ 
    const int size=1<<19; 
    int streambits=3; 
    int streamcount=1UL<<streambits; // # of output bins 
    int *instore=(int *)malloc(size*sizeof(int)); 
    int **outstore=(int **)malloc(streamcount*sizeof(int *)); 
    int **out=(int **)malloc(streamcount*sizeof(int)); 
    unsigned int seed=0; 

    for (int j=0; j<size; j++) instore[j]=j; 

    for (int i=0; i< streamcount; ++i) 
    outstore[i]=(int *)malloc(size*sizeof(int)); 

    int startTime=time(NULL); 
    for (int k=0; k<10000; k++) { 
    for (int i=0; i<streamcount; i++) out[i]=outstore[i]; 
    int *in=instore; 

    for (int j=0; j<size/2; j++) { 
     seed=seed*0x1234567+0x7162521; 
     int bin=seed>>(32-streambits); // pseudorandom destination bin 
     *(out[bin]++)=*(in++); 
     *(out[bin]++)=*(in++); 
    } 

    } 
    int endTime=time(NULL); 
    printf("Eval time=%ld\n", endTime-startTime); 
} 
+0

errr .. forse se c'era il codice? –

+0

Come scritto, quel codice non verrà compilato (punto e virgola mancante, che ho aggiunto), ma sono sospettoso di qualsiasi esempio che è stato modificato per la pubblicazione. –

risposta

4

Mentre stai scrivendo sui 64 raccoglitori di uscita, utilizzerai molte posizioni di memoria diverse. Se i raccoglitori sono riempiti essenzialmente a caso, significa che a volte avrai due contenitori che condividono la stessa linea di cache. Non è un grosso problema; la cache Core 2 L1 è associativa a 8 vie. Ciò significa che avrai un problema solo con la nona linea della cache. Con solo 65 riferimenti di memoria dal vivo in qualsiasi momento (1 lettura/64 scrittura), l'associazione 8 vie è OK.

La cache L2 è apparentemente a 12 vie (3/6 MB totali, quindi 12 non è un numero strano). Quindi, anche se avessi delle collisioni in L1, è molto probabile che tu non stia ancora toccando la memoria principale.

Tuttavia, se non ti piace, riordina i raccoglitori in memoria. Invece di accorciare sequenzialmente ciascun cestino, interlacciali. Per bin 0, memorizzare chunk 0-15 con offset 0-63, ma memorizzare chunk 16-31 con offset 8192-8255. Per il raccoglitore 1, memorizzare i pezzi da 0 a 15 sugli offset 64-127, ecc. Ciò richiede solo pochi cambiamenti e maschere, ma il risultato è che una coppia di contenitori condivide 8 linee di cache.

Un altro modo possibile per accelerare il codice in questo caso è SSE4, specialmente in modalità x64. Otterrai 16 registri x 128 bit e potrai ottimizzare la lettura (MOVNTDQA) per limitare l'inquinamento della cache. Non sono sicuro che ciò contribuirà molto alla velocità di lettura, tuttavia - mi aspetterei che il prefetcher di Core2 lo rilevasse. Leggere interi sequenziali è il tipo più semplice di accesso possibile, qualsiasi prefetcher dovrebbe ottimizzarlo.

+0

Quindi si sta tentando di mantenere ogni coda di output sempre mappata allo stesso cestino della cache. Ogni cestino della cache ha quindi sempre un numero uguale di stream, riducendo al minimo lo sfratto. Indirizzi casuali potrebbero facilmente mappare più di 9 flussi allo stesso bin e causare sfratti. Compatto e dipendente dalla CPU, ma logico! Grazie. – SPWorley

1

È possibile esplorare per mappare i file in memoria. In questo modo il kernel può prendersi cura della gestione della memoria per te. Il kernel di solito sa meglio come gestire le cache delle pagine. Ciò è particolarmente vero se l'applicazione deve essere eseguita su più di una piattaforma, in quanto i diversi Osos gestiscono la gestione della memoria in modi diversi.

Esistono framework come ACE (http://www.cs.wustl.edu/~schmidt/ACE.html) o Boost (http://www.boost.org) che consentono di scrivere codice che esegue il mapping della memoria in modo indipendente dalla piattaforma.

2

Ecco alcune idee se davvero ottiene disperata ...

Si potrebbe prendere in considerazione l'aggiornamento hardware. Per le applicazioni di streaming in qualche modo simili alle tue, ho scoperto che ho avuto un grande aumento di velocità passando a un processore i7. Inoltre, i processori AMD sono presumibilmente migliori rispetto al Core 2 per il lavoro legato alla memoria (sebbene non li abbia usati di recente da solo).

Un'altra soluzione che potreste considerare è l'elaborazione su una scheda grafica utilizzando un linguaggio come CUDA. Le schede grafiche sono sintonizzate per avere una larghezza di banda di memoria molto elevata e per eseguire calcoli matematici in virgola mobile. Aspettatevi di dedicare da 5 a 20 volte il tempo di sviluppo per il codice CUDA relativo a un'implementazione C lineare non ottimizzata.

3

Avete la possibilità di scrivere i vostri flussi di output come un unico flusso con metadati incorporati per identificare ogni "blocco"? Se dovessi leggere un "chunk", esegui la tua funzione threshhold su di esso, quindi invece di scriverlo su un particolare stream di output devi solo scrivere a quale stream apparteneva (1 byte) seguito dai dati originali, avresti seriamente riduci il tuo battere.

Non suggerirei questo a parte il fatto che hai detto che devi elaborare questi dati molte volte. In ogni esecuzione successiva, si legge il flusso di input per ottenere il numero del contenitore (1 byte), quindi si deve fare tutto ciò che è necessario fare per quel contenitore sui successivi 8 byte.

Per quanto riguarda il comportamento di cache di questo meccanismo, dal momento che si sta solo scorrendo attraverso due flussi di dati e, in tutto tranne il primo caso, scrivendo tutti i dati che si stanno leggendo, l'hardware fornirà tutto l'aiuto potresti sperare fino al prefetching, ottimizzazione della linea cache, ecc.

Se dovessi aggiungere quel byte in più ogni volta che hai elaborato i tuoi dati, il tuo caso peggiore di cache è il caso medio. Se puoi permetterti il ​​colpo di archiviazione, mi sembra una vittoria.

1

La vera risposta per situazioni come questa è quella di programmare diversi approcci e cronometrarli. Che hai ovviamente fatto. Tutte le persone come me possono fare è suggerire altri approcci per provare.

Per esempio: anche in assenza di cache di santa ragione (il vostro flussi di uscita mappatura per le stesse linee di cache), se si sta scrivendo interi dimensioni, con size = 1 < < 19 e sizeof (int) = 4, 32- bit - cioè se stai scrivendo 8 MB di dati, stai effettivamente leggendo 8 MB e poi scrivendo 8 MB. Perché se i tuoi dati sono in memoria WB (WriteBack) ordinaria su un processore x86, per scrivere su una riga devi prima leggere la vecchia copia della linea - anche se stai per buttare via i dati.

È possibile eliminare questo traffico di lettura RFO non necessario mediante (a) utilizzando la memoria WC (probabilmente un problema da impostare) o (b) utilizzando archivi di streaming SSE, ovvero archivi NT (non temporali). MOVNT * - MOVNTQ, MOVNTPS, ecc (C'è anche un MOVNTDQA lo streaming di carico, anche se più doloroso da usare.)

ho un po 'come questo scritto ho appena trovato da googling http://blogs.fau.de/hager/2008/09/04/a-case-for-the-non-temporal-store/

Ora: MOVNT * si applica ai WB memoria ma funziona come la memoria del WC, usando un piccolo numero di buffer di scrittura di cmbining. Il numero effettivo varia in base al modello del processore: c'erano solo 4 sul primo chip Intel ad averli, P6 (noto anche come Pentium Pro). Ooof ... Il 4KCC di Bulldozer (Write Combining Cache) fornisce fondamentalmente 64 buffer di combinazione di scrittura, per http://semiaccurate.com/forums/showthread.php?t=6145&page=40, sebbene ci siano solo 4 classici buffer di WC. Ma http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf dice che alcuni processos hanno 6 buffer di WC, e altri 8. Comunque ... ce ne sono alcuni, ma non così tanti. Di solito non 64.

Ma ecco qualcosa che si potrebbe provare: implementare scrivere combinando te stesso.

a) scrivere su un singolo set di 64 buffer (#streams), ciascuno della dimensione 64B (dimensione della linea della cache), - o forse 128 o 256B. Lascia che questi buffer si trovino nella normale memoria del WB. Puoi accedervi con negozi normali, anche se puoi usare MOVNT *, ottimo.

Quando uno di questi buffer si riempie, copiarlo come una raffica nel punto in memoria in cui si suppone che lo streaming debba andare. Utilizzo di negozi di streaming MOVNT *.

Questo finirà per fare * N byte memorizzati ai buffer temporanei, colpendo la cache L1 * 64 * 64 byte letti per riempire i buffer temporanei * N byte letti dal buffer temporanei, colpendo la cache L1. * N byte scritti tramite gli archivi di streaming: in pratica vanno direttamente alla memoria.

cioè N byte hit cache di lettura + n byte di cache in scrittura + N byte di cache miss

contro N byte cache di lettura mancata + N byte letti cache di scrittura.

Ridurre i N byte di cache miss read può far recuperare il sovraccarico extra.

Problemi correlati