Optymalizacja wykonania programów sekwencyjnych Krzysztof Banaś Obliczenia Wysokiej Wydajności 1
Optymalizacja sekwencyjna Przez optymalizację rozumie się zmiany dokonywane w kodzie źródłowym lub w trakcie tłumaczenia kodu źródłowego na język maszynowy mające na celu osiągnięcie pożądanych cech przez program wynikowy Optymalizacji kodu dokonuje się zazwyczaj ze względu na jeden z dwóch czynników: rozmiar kodu szybkość działania kodu (wydajność) Krzysztof Banaś Obliczenia Wysokiej Wydajności 2
Optymalizacja sekwencyjna Efekt optymalizacji wydajności programów uzyskuje się najczęściej poprzez: redukcję liczby wykonywanych operacji optymalizację dostępu do pamięci umożliwienie sprawniejszego przetwarzania potokowego przez procesor Ten ostatni cel może być uzyskiwany np. w efekcie usunięcia pojawiających się w programach zależności danych Krzysztof Banaś Obliczenia Wysokiej Wydajności 3
Optymalizacja sekwencyjna Optymalizację przeprowadzić można: ręcznie stosując odpowiednie techniki wykorzystując opcje optymalizującego kompilatora przekazując, jeśli jest taka możliwość, wykonanie części kodu procedurom zoptymalizowanych bibliotek Opłacalność wyboru jednego z powyższych sposobów zmienia się w czasie i zależy od szeregu czynników, takich jak np.: istnienie i jakość zoptymalizowanych bibliotek wiek i typowość sprzętu, na którym dokonywane są obliczenia typowość optymalizowanego programu Krzysztof Banaś Obliczenia Wysokiej Wydajności 4
Optymalizacja sekwencyjna Klasyczne sposoby optymalizacji obejmują szereg technik, które można stosować ręcznie i które realizowane są także przez optymalizujące kompilatory Zyski ze stosowania poszczególnych technik zależą i od szczegółowej struktury kodu, i od własności procesora, na którym przeprowadzane są obliczenia W korzystnych przypadkach optymalizacja sekwencyjna może prowadzić do kilkudziesięciokrotnego zmniejszenia czasu wykonania programów Krzysztof Banaś Obliczenia Wysokiej Wydajności 5
Klasyczne techniki optymalizacji Optymalizacja dotycząca zmiennych i wyrażeń: constant folding (zwijanie stałych) copy propagation (propagacja kopii) strength reduction (redukcja złożoności wyrażeń) variable renaming (przemianowanie zmiennych) common subexpression elimination (eliminacja powtarzających się podwyrażeń) Krzysztof Banaś Obliczenia Wysokiej Wydajności 6
Klasyczne techniki optymalizacji Optymalizacja wykonania pętli: induction variable simplification (uproszczenie wyrażeń zawierających indeks pętli) loop invariant code motion (usunięcie poza pętle kodu niezależnego od iteracji) loop interchange (zamiana kolejności wykonywania pętli) loop fusion (łączenie pętli) loop fission (rozdzielanie pętli) loop unrolling (rozwijanie pętli) blocking (grupowanie instrukcji ze względu na dostęp do pamięci podręcznej) Przykład: mat_vec Krzysztof Banaś Obliczenia Wysokiej Wydajności 7
Klasyczne techniki optymalizacji Optymalizacja na poziomie instrukcji: dead code removal (usuwanie nieosiągalnego lub produkującego zbędne dane kodu) tail recursion elimination (eliminacja rekursji ogonowej) inlining (wplatanie procedur rozwijanie w miejscu wywołania) software prefetching pobieranie z wyprzedzeniem realizowane programowo software pipelining przetwarzanie potokowe na poziomie kodu źródłowego i wiele innych Przykład: ddot Krzysztof Banaś Obliczenia Wysokiej Wydajności 8
Kompilatory optymalizujące Optymalizacja w trakcie kompilacji odbywa się najczęściej po analizie składniowej, przed generowaniem kodu pośredniego (object code) Kompilator operuje na formie kodu przetworzonej przez analizator składni do jednej z możliwych postaci, takich jak: drzewa, kod dwuadresowy, kod trójadresowy, odwrotna notacja polska itp. Krzysztof Banaś Obliczenia Wysokiej Wydajności 9
Przykład postaci pośredniej while( j < n ) { k = k + 2j; m = 2j; j++; } A: t1 := j; t2 := n t3 := t1 < t2 jmp (B) t3 jmp (C) B: t4 := k t5 := j t6 := t5 * 2 t7 := t4 + t6 k := t7 t8 := j t9 := t8 * 2 m := t9... jmp (A) C:... Krzysztof Banaś Obliczenia Wysokiej Wydajności 10
Kompilatory optymalizujące Analiza składniowa umożliwia nadanie programowi struktury, w której uwzględnia się: przepływ sterowania: możliwości przekazania sterowania z jednego punktu kodu do drugiego przepływ danych: określenie miejsca, w którym nadaje się wartość zmiennej i miejsc, w których się z tej wartości korzysta Analiza prowadzi do zapisu programu z użyciem: rejestrów bloków podstawowych Krzysztof Banaś Obliczenia Wysokiej Wydajności 11
Kompilatory optymalizujące Blok podstawowy: sekwencja instrukcji charakteryzująca się tym, że jeżeli wykonywana jest jedna z nich wykonywane są wszystkie z wnętrza bloku podstawowego nie można wyskoczyć (instrukcja skoku jest zawsze końcem bloku) do wnętrza bloku nie można wskoczyć (dowolna instrukcja opatrzona etykietą lub będąca celem skoku jest początkiem bloku podstawowego Krzysztof Banaś Obliczenia Wysokiej Wydajności 12
Przykłady kodu asemblera.l2.l4.l3 movl 4(%ebp), %eax // j > eax cmpl 12(%ebp), %eax // n <> eax? jl.l4 jmp.l3 movl 4(%ebp), %eax movl %eax, %edx leal 0(,%edx,2), %eax addl %eax, 8(%ebp) movl 4(%ebp), %eax movl %eax, %edx leal 0(,%edx,2), %eax movl %eax, 16(%ebp) incl 4(%ebp) jmp.l2 // j > eax // j >edx // eax=2*edx // k += eax (k+=2*j) // j >eax // j >edx // eax=2*edx // m=eax (m=2*j) // j++ Krzysztof Banaś Obliczenia Wysokiej Wydajności 13
Przykłady kodu asemblera.l2.l4.l3 movl 4(%ebp), %eax // j > eax cmpl 12(%ebp), %eax // n <> eax? jl.l4 jmp.l3 movl 4(%ebp), %eax movl %eax, %edx leal 0(,%edx,2), %eax addl %eax, 8(%ebp) movl 4(%ebp), %eax movl %eax, %edx leal 0(,%edx,2), %eax movl %eax, 16(%ebp) incl 4(%ebp) jmp.l2 // j > eax // j >edx // eax=2*edx // k += eax // j >eax // j >edx // eax=2*edx // m=eax // j++.l4 leal (%edx, %eax, 2), %edx // edx+=2*eax leal 0(,%eax,2), %ecx // ecx=2*eax incl %eax // eax+=1 cmpl %ebx, %eax // n<>eax? jl.l4 wersja 2 (IVS):.L4: addl $1, %ecx // j++ addl %eax, %edx // k+=m addl $2, %eax // m+=2 cmpl %r8d, %ecx // n<>j? jne.l4 Krzysztof Banaś Obliczenia Wysokiej Wydajności 14
Kompilatory optymalizujące Typowe opcje optymalizacji: poziomy optymalizacji (grupowanie różnych technik ze względu na czas działania i agresywność ingerencję w pierwotną strukturę kodu): zazwyczaj oznaczane O0,O1,itd. (najwyższe opcje oznaczają często zrównoleglenie kodu) szczegółowe techniki optymalizacji (przykłady z gcc): fstrength reduce, fcse follow jumps, ffast math, funroll loops, fschedule insns, finline functions, fomit frame pointer niektóre dostępne dla ręcznej optymalizacji, inne nie wektoryzacja (wykorzystanie rozkazów SIMD) zrównoleglenie (model z pamięcią wspólną, najczęściej poprzez uzupełnienie kodu dyrektywami OpenMP) Krzysztof Banaś Obliczenia Wysokiej Wydajności 15
Język asemblera AK Przykład: lista rozkazów IA 32 transfer danych: mov: przesunięcie (bez operacji pamięć pamięć) push, pop: operacje na stosie; ld, st load, store operacje arytmetyczne i logiczne: add, sub, mul: standardowe +,, * inc, dec, neg: ++1, 1, *=( 1) xor, and, or: działania logiczne (na bitach) leal: obliczenie adresu (bez transferu danych) cmp: obliczenie wyrażenia warunkowego przeniesienie sterowania jmp, jge, je, jl: skok bezwarunkowy i warunkowe call, ret: obsługa wywołań procedur Krzysztof Banaś Obliczenia Wysokiej Wydajności 16
Język asemblera AK Przykład: lista rozkazów IA 32 argumenty: bezpośrednie zawartość rejestrów zawartość komórek pamięci o obliczonym adresie obliczanie adresu: adres = base + index*scale + disp notacja AT&T: disp(base,index,scale) base i index są zawartościami rejestrów 32 bitowych disp i scale są liczbami (scale=1,2,4,8) Krzysztof Banaś Obliczenia Wysokiej Wydajności 17
Język asemblera AK Przykład: lista rozkazów IA 32 dostępne rejestry 32 bitowe: %eax (akumulator), %ebx, %ecx,..., %ebp (wskaźnik ramki), %esp (wskaźnik stosu) 16 bitowe: %ax (adresowanie połówki %eax), itd. 8 bitowe: %ah, %al (adresowanie połówek %ax), itd. segmentowe: %cs (kod), %ds (dane), %ss (stos), itd. kontrolne, uruchamiania (debugowania), testowe zmiennoprzecinkowe w postaci stosu: %st(0), %st(1), itd. Krzysztof Banaś Obliczenia Wysokiej Wydajności 18
Lista rozkazów x86_64 AK Powszechne obecnie rdzenie 64 bitowe Standardowe rejestry 64 bitowe Szereg optymalizacji ze względu na wydajność Większa liczba rejestrów (16: %rax %r15 ) Argumenty (do 6 całkowitych i do 8 zmiennoprzecinkowych) procedur przekazywane poprzez rejestry Nowe rozkazy operacji całkowitych (końcówka q dla argumentów 64 bitowych) i zmiennoprzecinkowych (końcówka sd) Rozbudowana liczba rozkazów wektorowych (operujących na 16 rejestrach 128 bitowych, %xmm00 %xmm15 ) Wzrost stopnia komplikacji kodu asemblera Dużo rozkazów manipulacji zmiennymi (np. cltq a.k.a. cdqe zmiana liczby z 32 na 64 bitową, inne zmieniające rozmiar danych) Krzysztof Banaś Obliczenia Wysokiej Wydajności 19
Język asemblera AK Pliki w języku assemblera: dwie notacje składni: firmy Intel i firmy AT&T składnia AT&T (przyjęta w asemblerze GNU): nazwa rozkazu zawiera jako ostatnią literę typ argumentu źródło danych poprzedza cel wyniku argumenty bezpośrednie poprzedzone są symbolem $ nazwy rejestrów poprzedzone są znakiem % osobne rozkazy procesora i koprocesora liczb zmiennopozycyjnych (te ostatnie zaczynają się literą f) Krzysztof Banaś Obliczenia Wysokiej Wydajności 20
Asembler GNU AK Pliki asemblera GNU (standardowy asembler Linuxa) pojedyncza linia pojedynczą instrukcją instrukcja etykieta:.dyrektywa_asemblera rozkaz_asemblera dyrektywy określają: pliki (.file), sekcje (segmenty) (.section), stałe (napisy, liczby, znaki) (...,.ascii,.float,...), wyrównania (.align), symbole wspólne (.comm), itp. rozkazy_asemblera odpowiadają rozkazom procesora Krzysztof Banaś Obliczenia Wysokiej Wydajności 21