Programowanie w asemblerze Optymalizacja

Transkrypt

Programowanie w asemblerze Optymalizacja
Programowanie w asemblerze
Optymalizacja
Zbigniew Jurkiewicz, Instytut Informatyki UW
17 stycznia 2017
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Przesłania warunkowe
Czasem dokonujemy porównania i zależnie od wyniku
chcemy dokonać pojedynczego przesłania.
Można wtedy użyć instrukcji przesłania warunkowego,
wykonywanego tylko gdy spełniony był wskazany warunek,
np. instrukcja
cmove eax,1
umieszcza 1 w rejestrze eax tylko wtedy, gdy
porównywane ostatnio elementy były równe.
Podstawowa zaleta to unikanie konieczności oczyszczania
potoku lub wykonania spekulacyjnego.
Przypisanie warunkowe SET
Przepisanie warunkowe CMOV
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Przesłania warunkowe: przykład
Znalezienie wiekszej
˛
z dwóch liczb (w EAX i EBX, wynik w
ECX):
mov ecx,eax
cmp ebx,ecx
cmova ecx,ebx
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Przesłania warunkowe: błedy
˛
Załóżmy, że kompilujemy w C wyrażenie
int *xp;
...
return (xp ? *xp : 0);
Jeśli xp jest w rdi, to można by użyć
xor eax,eax
test rdi,rdi
cmovne eax,[rdi]
;Być może zwrócimy zero
;xp == 0 ?
;Być może zwrócimy *xp
Ale wtedy dereferencja xp nastapi
˛ zawsze (nawet dla
wskaźnika NULL), a tego chcemy uniknać.
˛
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Unikanie skoków
Unikanie skoków to szerszy problem. Popatrzmy na obliczenie
wartości bezwzglednej
˛
test eax,eax
jns omiń
neg eax
;Ustawmy flagi
;znak dodatni
omiń:
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Unikanie skoków
Można to zrobić inaczej:
mov
sar
xor
sub
ecx,eax
ecx,31
eax,ecx
eax,ecx
;wsz˛
edzie bit znaku
;odwracamy bity
;odejmujemy -1 i mamy uzupełnienie do 2
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Potega
˛
2
Kolejna sztuczka: jak sprawdzić, czy liczba w EAX jest poteg
˛ a˛
dwójki?
mov ebx,eax
dec ebx
test eax,ebx
jnz niejest
;albo lea ebx,[eax - 1]
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Podpowiedzi
Procesor próbuje zgadywać, czy skok warunkowy bedzie
˛
wykonany.
Przy statycznym przewidywaniu zakłada, że skok „do tyłu”
bedzie
˛
wykonany.
Można mu podpowiadać używajac
˛ hintów: prefiksów
HT(0x3e) i i HNT(0x2e), np.
test ecx,ecx
db 3eh
jz L9
...
;HT = b˛
edzie skok
L9:
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Podpowiedzi
Cz˛esto nie warto trzymać danych w pamieci
˛ buforowej, jeśli
używane jednorazowo
Instrukcje zapisu bezpośredniego (non-temporal store)
MOVNTI, MOVNTPD itp. podczas zapisu do pamieci
˛
omijaja˛ cache.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Konserwatywność kompilatora
Kompilator C musi być konserwatywny i generować kod
tak, aby obejmował wszystkie możliwości
Przykład:
void memclr (char *dane, int n) {
for (; n > 0; n--)
*dane++ = 0;
}
Gdyby kompilator wiedział coś o wyrównaniu dane,
mógłby zerować naraz po 2, 4 a nawet 8 bajtów.
Musi jednak zakładać najgorszy przypadek.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Konserwatywność kompilatora
Istnieje kilka elementów C/++, które sa˛ wzorcowymi
spowalniaczami.
W czołówce jest konwersja (cast) z liczby rzeczywistej na
całkowita,
˛ np.
int i;
float f;
...
i = (int)f;
Taka konwersja to 50-100 cykli. Powód: standard C /C++
określa inny sposób zaokraglania
˛
niż używany w FPU,
wiec
˛ trzeba przełaczać
˛
tryb w koprocesorze.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Konserwatywność kompilatora
Inny kandydat do Oscara to pointer aliasing.
W poniższym kodzie kompilator nie wyciagnie
˛
obliczenia
p
+
2
przed
p
etl
˛
e
˛
*
void Func1 (int a[], int *p) {
int i;
for (i = 0; i < 100; i++)
a[i] = *p + 2;
}
I słusznie, bo (niech żyje C i C++ :-)
void Func2() {
int list[100];
Func1(list, &list[8]);
}
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Konserwatywność kompilatora
Czasem recepty sa˛ proste. Poniższy kod dwukrotnie
pobiera arg1->p1 z pamieci:
˛
struct S1 int p1;
struct S2 int p2, p3;
void f1 (struct S1 *arg1, struct S2 *arg2)
arg2->p2 += arg1->p1;
arg2->p3 += arg1->p1;
Musi tak być, bo arg2->p2 i arg1->p1 moga˛ być ta˛
sama˛ komórka˛ pamieci.
˛
A wystarczy wprowadzić zmienna˛ lokalna˛ i przypisać na
nia˛ S1->p1.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Asembler
Asembler pozwala korzystać z dostepu
˛
do usług niskiego
poziomu:
Rejestry i bezpośrednie wejście/wyjście
Omijanie konwencji kompilatora: inne przekazywanie
parametrów, naruszanie zasad przydziału pamieci,
˛
iteracyjne wołanie procedur
Łaczenie
˛
niezgodnych fragmentów kodu, np. zbudowanych
przez inne kompilatory
Reczna
˛
optymalizacja kodu w celu dopasowania do bardzo
konkretnej konfiguracji sprz˛etowej
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Skrajny przykład
Dla nabrania apetytu
Poniższy kod w C
float a[4], b[4], c[4];
for (int i = 0; i < 4; i++) {
c[i] = a[i] > b[i] ? a[i] : b[i];
}
można optymalnie zakodować nastepuj
˛ aco
˛
movaps xmm0,[a]
maxps xmm0,[b]
movaps [c],xmm0
;Load a vector
;max(a,b)
;c = a > b ? a : b
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Gdy brakuje rejestrów czyli „dwa w jednym”
Mamy dwie zmienne indeks i przyrost, obie 16-bitowe
(short)
Można je włożyć do jednego rejestru ARM, indeks u góry.
Wtedy kod w C
elem = tab[indeks];
indeks += przyrost;
zapisuje sie˛ w asemblerze jako
LDRB Relem, [Rtab, Rindprz, LSR#16]
ADD Rindprz, Rindprz, Rindprz, LSL#16
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Intel/AMD
Repertuar instrukcji procesorów CISC (x86) nie jest
optymalny — potwierdzenie to kilkakrotne zmiany filozofii
architektury.
Musi być zachowany z uwagi na wsteczna˛ kompatybilność
z systemami lat 1980, gdy pamieć
˛ RAM i dyskowa były
małe i kosztowne.
Ale CISC o dziwo ma także zalety. Zwiezłość
˛
kodu dobrze
pasuje do wymogów pamieci
˛ buforowych (cache) o
ograniczonych rozmiarach.
Główny problem procesorów x86 to mała liczba rejestrów,
troche˛ poprawiony przy projektowaniu x86-64.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Akceleratory grafiki
Wymagajace
˛ aplikacje graficzne potrzebuja˛ platformy z
koprocesorem do obsługi grafiki lub karta˛ akceleratora.
Moc obliczeniowa˛ tam zawarta˛ można wykorzystać także
do innych obliczeń, ale to temat na inne opowiadanie (i jest
to mocno zależne od sprz˛etu).
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Kod 64-bitowy
Zalety:
Wiecej
˛
rejestrów: nie trzeba trzymać zmiennych i wyników
pośrednich w pamiecu
˛ RAM.
Efektywne wywołania procedur: przekazywanie
parametrów w rejestrach.
64-bitowe rejestry do liczb całkowitych.
Lepsza gospodarka przydziałem dużych bloków pamieci.
˛
Wbudowany repertuar SSE2.
Wzgledna
˛
adresacja danych, wydajny kod relokowalny.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Kod 64-bitowy
Wady:
Dwa razy wieksze
˛
adresy i pozycje stosu: kłopoty z
pamieci
˛ a˛ buforowa.
˛
Dostep
˛ do statycznych i globalnych tablic wymaga wiecej
˛
instrukcji dla dużych obrazów pamieci.
˛ Dotyczy głównie
Windows i Maca.
Bardziej skomplikowane obliczanie adresu gdy rozmiar
wiekszy
˛
niż 2GB.
Niektóre instrukcje dłuższe.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Funkcje intrinsic w C++
Nowe podejście w łaczeniu
˛
kodu z różnych poziomów.
Funkcje intrinsic to znane kompilatorowi
wysokopoziomowe reprezentacje instrukcji maszynowych.
Przykład: dodawanie wektorów zmiennopozycyjnych
ADDPS w C++ można zapisać funkcja˛ _mm_add_ps.
Ponadto można zdefiniować odpowiednia˛ klase˛ wektorów i
przeciażyć
˛
dla niej operator +.
Funkcje intrinsic wystepuj
˛ a˛ w kompilatorach Microsoft,
Intela i GNU.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Ogladanie
˛
kodu z kompilatora
Różne powody:
Sprawdzanie, czy nie widać wyraźnych miejsc do recznego
˛
przepisania w asemblerze (lub przestawienia flag
kompilatora, np. -O3 ;-)
Potraktowanie kompilatora jako inteligentnej maszynistki, a
kodu jako wygodniejszej bazy niż pisanie od zera.
Ten kod co najmniej ma dobrze zrobione interfejsy z
otoczeniem, a tam cz˛esto najwiecej
˛
kłopotów.
A czasem wykryjemy bład
˛ w kompilatorze
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Kompilator Intel C++ (parallel composer)
Intrinsics dla wektorów, automatyczna wektoryzacja.
OpenMP i automatyczne zrównoleglanie watków.
˛
CPU dispatch: wersje dla różnych procesorów.
Najlepiej zoptymalizowane biblioteki matematyczne (choć
czasem nie umiały podzielić).
Wada: kod może wolniej działać na procesorach AMD i
VIA, należy wtedy pomijać dispatch.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Kompilator GNU
Intrinsics dla wektorów, automatyczna wektoryzacja.
OpenMP i automatyczne zrównoleglanie watków.
˛
Optymalizacja bibliotek czeka na swoja˛ kolej.
Ale akceptuje matematyczne biblioteki wektorowe AMD i
Intela.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Ograniczenia sprz˛etowe
Na ARM rejestry sa˛ 32-bitowe.
Należy unikać typów char i short dla liczników petli,
˛ bo
trzeba w kodzie recznie
˛
badać zakresy, np. dla instrukcji
short i;
...
i++;
kompilator za każdym razem musi badać, czy nie nastapiło
˛
przekroczenie zakresu i „przerzucać” na zero. Rejestry sa˛
bowiem 32-bitowe, wiec
˛ brak sygnalizacji
przepełnienia/przeniesienia dla 16 bitów.
Tu także kompilator jest bezbronny.
Oczywiście w procesorze x86 nie ma tych problemów.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Instrukcje zależne
Czas wykonania ciagu
˛ instrukcji zależnych (te same
argumenty i/lub wyniki) równy jest sumie ich latency —
wymaganej liczby cykli
Jeśli instrukcje sa˛ niezależne, to kolejna instrukcja
zaczyna sie˛ wcześniej i ten czas znaczaco
˛ maleje, np. kod
double list[100], sum = 0.;
for (int i = 0; i < 100; i++)
sum += list[i];
warto zastapić
˛ przez
double list[100], sum1 = 0., sum2 = 0., sum3 = 0., sum4 =
for (int i = 0; i < 100; i += 4) {
sum1 += list[i];
sum2 += list[i+1];
sum3 += list[i+2];
sum4 += list[i+3];
}
sum1 = (sum1 + sum2) + (sum3 + sum4);
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Zależności
Czasem wyglada
˛ to dziwnie, na przykład instrukcje˛
przypisania
y = a + b + c + d;
warto zastapić
˛ przez
y = (a + b) + (c + d);
Specyfikacja wielu jezyków
˛
programowania nakłada
wymóg wykonywania od lewej do prawej (np. żeby błedy
˛
zaokragle
˛ ń były zawsze takie same) i kompilator nic wtedy
nie może zrobić.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Rejestry cz˛eściowe
Niektóre CPU robia˛ out of order execution ale nie sa˛ w
stanie przemianować rejestrów cz˛eściowych (ax, ah, al).
Powoduje to opóźnienie w poniższym kodzie, ponieważ
trzecia instrukcja musi czekać na górne 16 bitów z
mnożenia
imul eax,6
mov [mem2],eax
mov ax,[mem3]
add ax,2
mov [mem4],ax
;operandy 16-bitowe
Jeśli zastapimy
˛
te˛ instrukcje˛ przez
movzx eax,[mem3]
to zależność zostaje zlikwidowana.
Pewnie dlatego w trybie 64-bitowym dzieje sie˛ to
automatycznie przy przesłaniach 32-bitowych.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Zmiany kolejności
Głównie na mocno potokowanych RISCach (np. ARM),
wymuszone specyfika˛ procesora
Na ARM9TDMI dla instrukcji ładowania z pamieci
˛ (np.
LDR) nie należy przez dwa cykle używać załadowanej
wartości.
Mnożenie trwa tyle samo co mnożenie z akumulacja˛
(MLA). Wniosek oczywisty.
Na ARM10E instrukcje wielokrotnego ładowania z pamieci
˛
i zapisywania do niej działaja˛ „w tle”. Pozornie wiec
˛
zajmuja˛ jeden cykl, o ile nie próbujemy używać tych
rejestrów w kolejnej instrukcji.
Na Intel XScale instrukcja LDRD ładuje dwa słowa naraz w
jednym cyklu. Ale nie należy używać pierwszego rejestru
przez dwa kolejne cykle, a drugiego przez trzy.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Skoki i procedury
Pobieranie kodu po (nieoczekiwanym) skoku generuje
opóźnienia rz˛edu 1-3 cykli.
Najwieksze,
˛
gdy adres docelowy wypada pod koniec
16-bajtowego bloku (ramka). Paradoks: warto czasem
wcześniej w kodzie zastapić
˛ krótsza˛ postać instrukcji
dłuższa,
˛ aby osiagn
˛ ac
˛ wyrównanie.
Do przewidywania powrotów z procedur (ret) służy tzw.
return stack buffer, zwykle o rozmiarze do 16 elementów.
Nie należy ogłupiać mechanizmu wyskakujac
˛ z procedur
czy też potajemnie zdejmujac
˛ adresy powrotne ze stosu
(albo używać ret jako skoku pośredniego).
Wywołania redukcyjne (tail calls) robi sie˛ przez skoki!
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Metaprogramowanie
Zamiast pisać pokretne
˛
makra asemblera albo nadużywać m4
lepiej pisać programy, które generuja˛ inne programy lub ich
cz˛eści:
Generatory tablic sinusów, cosinusów albo lat
przestepnych
˛
Przetwarzajace
˛ plik binarny na postać źródłowa˛
Zamieniajace
˛ bitmapy na procedury szybkiego
wyświetlania
Wydobywajace
˛ różne aspekty z tego samego kodu
Specjalizowany kod w asemblerze na podstawie skryptu w
Scheme lub innym jezyku
˛
i dodatkowych ograniczeń.
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja
Tuning: narz˛edzia
AMD Code Analyst
Intel VTune
New-Jersey Machine-Code Toolkit (w ML)
http://www.eecs.harvard.edu/ nr/toolkit/
Zbigniew Jurkiewicz, Instytut Informatyki UW
Programowanie w asemblerze Optymalizacja