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?
Se si sta eseguendo la compilazione con l'ottimizzazione attivata, sarà necessario esaminare l'assieme per sapere veramente cosa sta succedendo. – RussS
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. –