Czasy dosteępu do pamięci to w przybliżeniu
Zmiana kolejności indeksowania może spowodować, że kod działa nawet 10 razy szybciej z uwagi na lepsze wykorzystanie pamięci podręcznej
// zapis kolumnami int array[256][256]; for( i=0; i<256; ++i) for( j=0; j<256; ++j) array[j][i] = i*j;
// zapis wierszami - zgodnie z ułożeniem w pamięci int array[256][256]; for( i=0; i<256; ++i) for( j=0; j<256; ++j) array[i][j] = i*j;
Wybieraj instrukcję która ma mniejszy opcode i mniej operandów. Wtedy cache'owanie instrukcji i dekodowanie jest wydajniejsze.
Używaj zmiennych lokalnych zamiast globalnych
Wyrównanie przyspiesza dostęp do pamięci.
Szczególnie ważne:
Stos powinien być wyrównany do granicy 16 bajtów przed wywołaniem funkcji
w większości systemów operacyjnych.
Dobrze jest tak ułożyć lokalne zmienne aby były wyrównane do odpowiednich granic.
Bezpośrednie wyrównanie stosu
funkcja: ; jeden argument na stosie (32-bit) push ebp mov ebp, esp sub esp, zmienneLokalne and esp, 0FFFFFFF0H ; wyrównanie do 16b mov eax, [ebp+8] ; kopiowanie movdqu xmm0,[eax] ; z niewyrównanej tablic movdqa [esp],xmm0 ; do wyrównanej zmiennej lokalnej call SomeOtherFunction ... mov esp, ebp pop ebp ret
Możemy tak dobrać rozmiar danych lokalnych aby stos pozostał wyrównany
funkcja: ; (32-bit) sub esp, 28 mov eax, [esp+32] movdqu xmm0,[eax] movdqa [esp],xmm0 call SomeOtherFunction ... add esp, 28 ret
Jeżeli odkładamy wcześniej jakieś rejestry na stos to musimy je też wliczyć.
W kodzie 64 bitowym adresy są 8 bajtowe.
funkcja: ; (64-bit) sub rsp, 24 mov rax, [rsp+32] movdqu xmm0,[rax] movdqa [rsp],xmm0 call SomeOtherFunction ... add rsp, 24 ret
Okazuje się, że dzieki superskalarności zamiast
push eax push ebx push ecx
sub esp, 12 mov eax, [esp+8] mov ebx, [esp+4] mov ecx, [esp]
sub esp, 12 mov eax, [esp+8] mov ebx, [esp+4] mov ecx, [esp]
Jeżeli dane są wykorzystywane wielokrotnie albo wykorzystujemy wielu danych z ciągłęgo obszaru dobrze jest wczesniej pobrać je do odpowiedniego poziomu pamięci podręcznej. Służą do tego instrukcje
PREFETCH0 mem ; załaduj do wszystkich poziomów cache PREFETCH1 mem ; załaduj do wszystich poziomów poza L1 PREFETCH2 mem ; ... poza L1 i L2
Używaj PREFETCH wewnątrz pętli. Rozwiń częściowo pętlę tak aby w jednej iteracji "zużyć" cały wiersz cache'u.
mov rbx, tab mov rcx, [n] .loop: PREFETCH [rbx + 16] add eax, [rbx] add ebx, [rbx+4] add dcx, [rbx+8] add esi, [rbx+12] add rbx, 16 sub rcx, 4 jg .loop
We współczesnych procesorach mechanizmy przewidywania rozgałęzień i wstępnego pobierania danych i kodu działają tak dobrze, że dają często lepsze rezultaty niż ręczna optymalizacja.
Nietrafienia w cache są szczególnie kosztowne przy zapisach. Cały wiersz musi zostać wczytany, zmodyfikowany i zapisany do pamięci. Jeżeli zapisujemy dane, które z dużym prawdopodobieństwem nie będą ponownie wczytywane można użyć instrukcji które pomijają cache (non-temporal write) MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPD, MOVNTPS.
Instrukcje te są jednak dość kosztowne i dają zysk jeżeli zapisuje się blok pamięci większy niż połowa rozmiaru największego cache'a.