2012-01-30 2 views
7

Sto cercando di ottimizzare un algoritmo ad alta intensità di calcolo e sono un po 'bloccato in qualche problema di cache. Ho un buffer enorme che viene scritto occasionalmente e in modo casuale e letto solo una volta alla fine dell'applicazione. Ovviamente, scrivere nel buffer produce molti errori di cache e inoltre inquina le cache che sono poi necessarie di nuovo per il calcolo. Ho provato a utilizzare le instrinsics di movimento non temporali, ma i problemi di cache (riportati da valgrind e supportati dalle misurazioni di runtime) si verificano ancora. Tuttavia, per approfondire le mosse non temporali, ho scritto un piccolo programma di test, che puoi vedere qui sotto. Accesso sequenziale, buffer di grandi dimensioni, solo scritture.Perché _mm_stream_ps produce mancanze di cache L1/LL?

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#include <smmintrin.h> 

void tim(const char *name, void (*func)()) { 
    struct timespec t1, t2; 
    clock_gettime(CLOCK_REALTIME, &t1); 
    func(); 
    clock_gettime(CLOCK_REALTIME, &t2); 
    printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec)/1000000000); 
} 

const int CACHE_LINE = 64; 
const int FACTOR = 1024; 
float *arr; 
int length; 

void func1() { 
    for(int i = 0; i < length; i++) { 
     arr[i] = 5.0f; 
    } 
} 

void func2() { 
    for(int i = 0; i < length; i += 4) { 
     arr[i] = 5.0f; 
     arr[i+1] = 5.0f; 
     arr[i+2] = 5.0f; 
     arr[i+3] = 5.0f; 
    } 
} 

void func3() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 4) { 
     _mm_stream_ps(&arr[i], buf); 
    } 
} 

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[4], buf); 
     _mm_stream_ps(&arr[8], buf); 
     _mm_stream_ps(&arr[12], buf); 
    } 
} 

int main() { 
    length = CACHE_LINE * FACTOR * FACTOR; 

    arr = malloc(length * sizeof(float)); 
    tim("func1", func1); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func2", func2); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func3", func3); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func4", func4); 
    free(arr); 

    return 0; 
} 

La funzione 1 è l'approccio naive, la funzione 2 utilizza lo srotolamento del ciclo. La funzione 3 utilizza movnt, che in effetti è stata inserita nell'assembly almeno quando ho controllato per -0. Nella funzione 4 ho provato a pubblicare diverse istruzioni movntps per aiutare la CPU a combinare la scrittura. Ho compilato il codice con gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c dove X è uno dei [0..3]. I risultati sono .. interessante da dire al meglio:

-O0 
func1 : 0.407794 s. 
func2 : 0.320891 s. 
func3 : 0.161100 s. 
func4 : 0.401755 s. 
-O1 
func1 : 0.194339 s. 
func2 : 0.182536 s. 
func3 : 0.101712 s. 
func4 : 0.383367 s. 
-O2 
func1 : 0.108488 s. 
func2 : 0.088826 s. 
func3 : 0.101377 s. 
func4 : 0.384106 s. 
-O3 
func1 : 0.078406 s. 
func2 : 0.084927 s. 
func3 : 0.102301 s. 
func4 : 0.383366 s. 

Come si può vedere _mm_stream_ps è un po 'più veloce rispetto agli altri quando il programma non è ottimizzato da gcc, ma poi non riesce in modo significativo il suo scopo quando l'ottimizzazione gcc è acceso . Valgrind riporta ancora molti errori di scrittura nella cache.

Quindi, le domande sono: Perché quelle mancanze di cache (L1 + LL) si verificano ancora anche se sto utilizzando le istruzioni di streaming NTA? Perché è in particolare func4 così lento ?! Qualcuno può spiegare/speculare cosa sta succedendo qui?

+2

Se si sta eseguendo la compilazione con l'ottimizzazione attivata, sarà necessario esaminare l'assieme per sapere veramente cosa sta succedendo. – RussS

+0

Sto osservando l'assembly, che diventa sempre più difficile da leggere con ogni livello di ottimizzazione, ma non mi dice perché il suggerimento non temporale viene ignorato. Almeno suppongo che venga ignorato poiché valgrind riporta ancora errori di cache in cui non mi aspetto nulla. Ad ogni modo, so che la domanda è piuttosto aspecifica, quindi apprezzerei davvero qualsiasi input su cosa potrebbe accadere qui. –

risposta

8
  1. Probabilmente, le vostre misure di riferimento per lo più le prestazioni della memoria di allocazione, non solo le prestazioni di scrittura. Il sistema operativo può allocare pagine di memoria non in malloc, ma al primo tocco, all'interno delle funzioni func*. Il sistema operativo può anche eseguire alcuni shuffle di memoria dopo che è stata allocata una grande quantità di memoria, quindi qualsiasi benchmark, eseguito subito dopo l'allocazione della memoria, potrebbe non essere affidabile.
  2. Il tuo codice ha il problema aliasing: il compilatore non può garantire che il puntatore dell'array non cambi nel processo di riempimento di questo array, quindi deve sempre caricare il valore arr invece di utilizzare un registro. Questo potrebbe costare un calo delle prestazioni. Il modo più semplice per evitare l'aliasing è copiare arr e length in variabili locali e utilizzare solo variabili locali per riempire l'array. Ci sono molti consigli noti per evitare variabili globali. L'aliasing è uno dei motivi.
  3. _mm_stream_ps funziona meglio se la matrice è allineata di 64 byte. Nel tuo codice non è garantito alcun allineamento (in realtà, malloc lo allinea di 16 byte). Questa ottimizzazione è evidente solo per i cortocircuiti.
  4. È consigliabile chiamare _mm_mfence dopo aver terminato con _mm_stream_ps. Questo è necessario per la correttezza, non per le prestazioni.
+1

Grazie mille Evgeny! 1. Questo è tutto. Non ne ero consapevole. Quando ho cambiato il codice per allocare la memoria una sola volta, ha modificato drasticamente i runtime in quello che mi aspettavo inizialmente. func3 + 4 sono circa 2-3 volte più veloci di func1 + 2. 2. Puoi approfondire questo aspetto? Ho pensato che l'aliasing sarebbe solo un problema riguardante la memoria fisica della memoria virtuale <->. Non vedo dove questo è un problema qui. 3. Ok, quindi dovrei usare valloc() o qualche altra funzione specifica di libc? Non ha avuto alcun impatto sui tempi di esecuzione. L'allineamento della linea della cache dovrebbe aiutare la cpu con la combinazione di scrittura, vero? 4. Ok. –

+1

Ho aggiunto alcune spiegazioni sull'aliasing e sul link wikipedia. L'allineamento della linea cache consente di utilizzare correttamente la combinazione di scrittura per i primi 64 byte dell'array. Per l'allineamento puoi usare diverse funzioni dipendenti dalla piattaforma, non mi ricordo di tutte loro adesso. Oppure puoi usare il trucco '(p + 63) & ~ 63'. O semplicemente ignorare l'allineamento se i tuoi array sono sempre più grandi di megabyte. –

+1

Riguardo al problema di aliasing, dovresti cercare di passare "arr" e "length" come argomenti alle tue funzioni, invece di averli come globali. Questo * potrebbe * migliorare le opportunità di ottimizzazione per il compilatore. – rotoglup

2

Non dovrebbe essere questo Func4:

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[i+4], buf); 
     _mm_stream_ps(&arr[i+8], buf); 
     _mm_stream_ps(&arr[i+12], buf); 
    } 
} 
+0

Hai ragione.Grazie :-) Questo porta a circa gli stessi risultati di func3. –