Próbuję zoptymalizować algorytm intensywny obliczeń i jestem w pewien sposób utknąć w jakimś problem z pamięcią podręczną. Mam ogromny bufor, który jest zapisywany od czasu do czasu i losowo, i czytany tylko raz na końcu aplikacji. Oczywiście zapisanie do bufora powoduje wiele błędów w pamięci podręcznej, a ponadto zanieczyszcza pamięci podręczne, które następnie są potrzebne do obliczeń. Próbowałem użyć instrinetyki ruchu nieprzewidzianego, ale nadal występują błędy pamięci podręcznej (zgłaszane przez valgrind i obsługiwane przez pomiary czasu wykonywania). Jednakże, aby dokładniej zbadać nie-czasowe ruchy, napisałem mały program testowy, który można zobaczyć poniżej. Sekwencyjny dostęp, duży bufor, tylko zapisuje.Dlaczego _mm_stream_ps powoduje błędy pamięci podręcznej 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;
}
Funkcja 1 jest podejściem naiwnym, funkcja 2 używa rozwijania pętli. Funkcja 3 używa movntps, który w rzeczywistości został wstawiony do zespołu co najmniej gdy sprawdziłem dla -O0. W funkcji 4 próbowałem wydać kilka instrukcji movntps naraz, aby pomóc CPU w połączeniu zapisu. Skompilowałem kod pod numerem gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c
, gdzie X
jest jednym z [0..3]. Wyniki są .. ciekawego do powiedzenia w najlepszym wypadku:
-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.
Jak widać _mm_stream_ps jest trochę szybciej niż inni, gdy program nie jest optymalizowany przez gcc ale potem znacznie zawodzi swój cel, gdy gcc optymalizacja jest włączony . Valgrind nadal zgłasza wiele błędów w pamięci podręcznej.
Pytania są następujące: Dlaczego te (L1 + LL) chybienia pamięci podręcznej nadal występują, nawet jeśli używam instrukcji przesyłania strumieniowego NTA? Dlaczego szczególnie func4 jest taki powolny ?! Czy ktoś może wyjaśnić/spekulować, co się tutaj dzieje?
Jeśli kompilujesz z włączoną optymalizacją, musisz spojrzeć na zespół, aby naprawdę wiedzieć, co się dzieje. – RussS
Patrzę na zestaw, który btw staje się coraz trudniejszy do odczytania z każdym poziomem optymalizacji, ale nie wyjaśnia mi, dlaczego podpowiedź nie jest tymczasowa. Przynajmniej myślę, że jest to ignorowane, ponieważ valgrind wciąż zgłasza chybienie w pamięci podręcznej tam, gdzie nie oczekuję żadnego. W każdym razie, wiem, że pytanie jest raczej niespecyficzne, więc naprawdę doceniam wszelkie informacje na temat tego, co może się tu wydarzyć. –