2012-06-20 17 views
9

Il codice senza fissione è simile al seguente:Perché la fissione del loop ha senso in questo caso?

int check(int * res, char * map, int n, int * keys){ 
    int ret = 0; 
    for(int i = 0; i < n; ++i){ 
     res[ret] = i; 
     ret += map[hash(keys[i])] 
    } 
    return ret; 
} 

Con fissione:

int check(int * res, char * map, int n, int * keys){ 
    int ret = 0; 
    for(int i = 0; i < n; ++i){ 
     tmp[i] = map[hash(keys[i])]; 
    } 
    for(int i = 0; i < n; ++i){ 
     res[ret] = i; 
     ret += tmp[i]; 
    } 
    return ret; 
} 

Note:

  • Il collo di bottiglia è map[hash(keys[i])] che accede alla memoria in modo casuale.

  • normalmente, sarebbe if(tmp[i]) res[ret++] = i; per evitare il se, sto usando ret += tmp[i].

  • map[..] è sempre 0 o 1

La versione fissione è di solito molto più veloce e sto cercando di spiegare perché. La mia ipotesi migliore è che ret += map[..] introduca ancora alcune dipendenze e che impedisca l'esecuzione speculativa.

Mi piacerebbe sapere se qualcuno ha una spiegazione migliore.

+1

Ho pensato di dirlo. Sebbene questa domanda sia molto simile a [questa domanda] (http://stackoverflow.com/questions/8547778/why-is-one-loop-so-much-slower-than-two-loops), non sembra un duplicato – Mysticial

+0

Sono finalmente in grado di costruire un caso di test che riproduca i risultati ... Ora per vedere cosa posso farne. – Mysticial

+0

@Mysticial dovresti essere in grado di vedere che di solito il codice di fissione è molto più veloce. è solo più lento o altrettanto veloce quando la mappa non è molto grande, ad es. quando mappa, chiavi e res si inseriscono tutti nella cache – user16367

risposta

8

Dai miei test, ottengo circa 2 volte la differenza di velocità tra i loop fusi e split. Questa differenza di velocità è molto costante, non importa quanto modifico il loop.

Fused: 1.096258 seconds 
Split: 0.562272 seconds 

(Fare riferimento alla parte inferiore per il codice di prova completa.)


Anche se non sono sicuro al 100%, ho il sospetto che questo è dovuto ad una combinazione delle due cose:

  1. Saturazione del buffer di memoria di caricamento per memory disambigutation a causa di mancanze della cache da map[gethash(keys[i])].
  2. Una dipendenza aggiunta nella versione del loop fuso.

È ovvio che lo map[gethash(keys[i])] causerà un errore di cache quasi ogni volta. In effetti, è probabilmente sufficiente per saturare l'intero buffer del load-store.

Ora diamo un'occhiata alla dipendenza aggiunta. Il problema è la variabile ret:

int check_fused(int * res, char * map, int n, int * keys){ 
    int ret = 0; 
    for(int i = 0; i < n; ++i){ 
     res[ret] = i; 
     ret += map[gethash(keys[i])]; 
    } 
    return ret; 
} 

La variabile ret è necessario per la risoluzione degli indirizzi del negozio res[ret] = i;.

  • Nel loop fuso, ret proviene da una mancanza di cache sicura.
  • Nel ciclo di divisione, ret è in arrivo tmp[i] - che è molto più veloce.

Questo ritardo nella risoluzione indirizzo del caso anello fuso probabile causa res[ret] = i memorizzare intasare il buffer load-store insieme map[gethash(keys[i])].

Dal momento che il buffer di carico-store ha una dimensione fissa, ma si ha il doppio della spazzatura in esso:
si è solo in grado di sovrapporre la cache manca la metà di quello di prima. Quindi 2 rallentamenti.


Suppongo che se abbiamo cambiato il ciclo fusa a questo:

int check_fused(int * res, char * map, int n, int * keys){ 
    int ret = 0; 
    for(int i = 0; i < n; ++i){ 
     res[i] = i; // Change "res" to "i" 
     ret += map[gethash(keys[i])]; 
    } 
    return ret; 
} 

Questo romperà la dipendenza risoluzione degli indirizzi.

(Si noti che non è più lo stesso, ma è solo per dimostrare la differenza di prestazioni.)

allora otteniamo tempi simili:

Fused: 0.487477 seconds 
Split: 0.574585 seconds 

Ecco il test completo codice:

#define SIZE 67108864 

unsigned gethash(int key){ 
    return key & (SIZE - 1); 
} 

int check_fused(int * res, char * map, int n, int * keys){ 
    int ret = 0; 
    for(int i = 0; i < n; ++i){ 
     res[ret] = i; 
     ret += map[gethash(keys[i])]; 
    } 
    return ret; 
} 
int check_split(int * res, char * map, int n, int * keys, int *tmp){ 
    int ret = 0; 
    for(int i = 0; i < n; ++i){ 
     tmp[i] = map[gethash(keys[i])]; 
    } 
    for(int i = 0; i < n; ++i){ 
     res[ret] = i; 
     ret += tmp[i]; 
    } 
    return ret; 
} 


int main() 
{ 
    char *map = (char*)calloc(SIZE,sizeof(char)); 
    int *keys = (int*)calloc(SIZE,sizeof(int)); 
    int *res = (int*)calloc(SIZE,sizeof(int)); 
    int *tmp = (int*)calloc(SIZE,sizeof(int)); 
    if (map == NULL || keys == NULL || res == NULL || tmp == NULL){ 
     printf("Memory allocation failed.\n"); 
     system("pause"); 
     return 1; 
    } 

    // Generate Random Data 
    for (int i = 0; i < SIZE; i++){ 
     keys[i] = (rand() & 0xff) | ((rand() & 0xff) << 16); 
    } 

    printf("Start...\n"); 

    double start = omp_get_wtime(); 
    int ret; 

    ret = check_fused(res,map,SIZE,keys); 
// ret = check_split(res,map,SIZE,keys,tmp); 

    double end = omp_get_wtime(); 

    printf("ret = %d",ret); 
    printf("\n\nseconds = %f\n",end - start); 

    system("pause"); 
} 
+0

Questa è una preziosa analisi, grazie. Quindi, la prima mappa [hash (chiave)] viene inserita nella coda di caricamento. Ora, non sono sicuro di cosa succederà dopo. La CPU sta andando a mettere res [ret] nella coda del negozio, con il vecchio valore ret e in seguito a rieseguire questo e causare il rallentamento? Oppure, attenderà il carico e inserirà la ris corretta [ret] nella coda del negozio. – user16367

+1

Questo è un dettaglio di basso livello di cui non sono sicuro (e probabilmente possibilmente proprietario di Intel). Sicuramente non è causato dalla predizione errata di 'ret'. (I tempi sono gli stessi anche quando 'ret' è sempre' 0' o '1'). Quindi sospetto che quest'ultimo sia più vicino. Forse non può entrare nel buffer del negozio fino a quando l'indirizzo non è conosciuto e disambiguato, quindi esegue il backup dell'intero buffer di riordino delle istruzioni. – Mysticial

1

Non penso che sia l'indicizzazione dell'array, ma la chiamata alla funzione hash() che potrebbe causare uno stallo della pipeline e impedire un ottimale riordino delle istruzioni.

+0

map [..] restituisce 0 o 1 quindi l'accesso a res è sequenziale. Puoi spiegare perché l'hash causerebbe uno stallo? l'hash è in realtà un #define, ma anche se fosse una funzione, perché si fermerebbe senza fissione? – user16367

+0

Inoltre, le chiamate attraverso 'map []' e 'hash()' possono espellere abbastanza della cache che gli accessi tramite 'res []' mancano tutti. Il secondo ciclo dopo la fissione avrebbe quindi un tasso di successo significativamente migliore. Ma probabilmente è un po 'soggettivo, a seconda di quanto sia grande "n". I casi piccoli potrebbero non vedere alcun miglioramento. – twalberg

+0

n sarebbe tra 500 e 1000 nel mio caso, in modo che le chiavi e le res si inseriscano nella cache. la mappa è solitamente grande e non si adatta completamente alla cache. – user16367

Problemi correlati