2012-01-30 9 views
7

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?

+2

Jeśli kompilujesz z włączoną optymalizacją, musisz spojrzeć na zespół, aby naprawdę wiedzieć, co się dzieje. – RussS

+0

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ć. –

Odpowiedz

8
  1. Prawdopodobnie twoi Benchmark środki głównie skuteczności alokacji pamięci, nie tylko pisać wydajność. Twój system operacyjny może przydzielać strony pamięci nie w malloc, ale po pierwszym dotknięciu wewnątrz funkcji func*. System operacyjny może również wykonywać tasowanie pamięci po przydzieleniu dużej ilości pamięci, więc wszelkie testy porównawcze wykonywane tuż po przydzieleniu pamięci mogą nie być niezawodne.
  2. Twój kod ma aliasing problem: kompilator nie może zagwarantować, że wskaźnik tablicy nie zmieni się w procesie wypełniania tej tablicy, więc musi zawsze ładować wartość arr z pamięci, zamiast używać rejestru. Może to spowodować obniżenie wydajności. Najprostszym sposobem uniknięcia aliasingu jest skopiowanie arr i length do zmiennych lokalnych i użycie jedynie zmiennych lokalnych do wypełnienia tablicy. Istnieje wiele dobrze znanych wskazówek pozwalających uniknąć zmiennych globalnych. Aliasing jest jednym z powodów.
  3. _mm_stream_ps działa lepiej, jeśli tablica jest wyrównana do 64 bajtów. W twoim kodzie nie jest gwarantowane wyrównanie (właściwie malloc wyrównuje je o 16 bajtów). Ta optymalizacja jest zauważalna tylko dla krótkich tablic.
  4. Po zakończeniu pracy z _mm_stream_ps warto zadzwonić pod numer _mm_mfence. Jest to potrzebne dla poprawności, a nie dla wydajności.
+1

Wielkie dzięki Jewgienij! 1. To jest to. Nie wiedziałem o tym. Kiedy zmieniłem kod, aby przydzielić pamięć tylko raz, dramatycznie zmieniłem środowiska wykonawcze na to, co początkowo oczekiwałem. func3 + 4 są około 2-3 razy szybsze niż func1 + 2. 2. Czy możesz rozwinąć nieco więcej? Myślałem, że aliasing będzie tylko problemem z pamięcią fizyczną pamięci wirtualnej <->. Nie widzę, gdzie to jest problem. 3. Ok, więc musiałbym użyć valloc() lub jakiejś innej specyficznej funkcji biblioteki libc? Nie miało żadnego wpływu na środowiska wykonawcze. Wyrównanie linii pamięci podręcznej powinno pomóc procesorowi z łączeniem zapisu, czy mam rację? 4. Ok. –

+1

Dodałem kilka wyjaśnień na temat aliasingu, a także łącza do Wikipedii. Wyrównanie linii pamięci podręcznej pozwala poprawnie używać kombinacji zapisu dla pierwszych 64 bajtów tablicy. Do wyrównania można użyć wielu funkcji zależnych od platformy, nie pamiętam ich wszystkich w tej chwili. Lub możesz użyć sztuczki '(p + 63) i ~ 63'. Lub po prostu zignoruj ​​wyrównanie, jeśli twoje tablice są zawsze większe niż megabajty. –

+1

W kwestii aliasingu powinieneś spróbować przekazać "arr" i "length" jako argumenty do swoich funkcji, zamiast mieć je jako globalne. To * może * poprawić możliwości optymalizacji kompilatora. – rotoglup

2

Nie powinno być to 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

Masz rację.Dzięki :-) To zabiera func4 do mniej więcej takich samych rezultatów jak func3. –

Powiązane problemy