Programowanie w asemblerze Optymalizacja

Podobne dokumenty
Architektura komputerów

Metody Realizacji Języków Programowania

Ćwiczenie nr 3. Wyświetlanie i wczytywanie danych

Programowanie w asemblerze Środowiska 64-bitowe

Adam Kotynia, Łukasz Kowalczyk

Struktura i działanie jednostki centralnej

Architektura systemów komputerowych Laboratorium 14 Symulator SMS32 Implementacja algorytmów

Podstawy programowania. Wykład 6 Wskaźniki. Krzysztof Banaś Podstawy programowania 1

MMX i SSE. Zbigniew Koza. Wydział Fizyki i Astronomii Uniwersytet Wrocławski. Wrocław, 10 marca Zbigniew Koza (WFiA UWr) MMX i SSE 1 / 16

4 Literatura. c Dr inż. Ignacy Pardyka (Inf.UJK) ASK MP.01 Rok akad. 2011/ / 24

Kompilator języka C na procesor 8051 RC51 implementacja

Programowanie Niskopoziomowe

Ćwiczenie 3. Konwersja liczb binarnych

Podstawy programowania w języku C i C++

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

002 Opcode Strony projektu:

Programowanie w asemblerze Środowiska 64-bitowe

Procesor ma architekturę rejestrową L/S. Wskaż rozkazy spoza listy tego procesora. bgt Rx, Ry, offset nand Rx, Ry, A add Rx, #1, Rz store Rx, [Rz]

Wskaźniki i dynamiczna alokacja pamięci. Spotkanie 4. Wskaźniki. Dynamiczna alokacja pamięci. Przykłady

Architektura Systemów Komputerowych, Wydział Informatyki, ZUT

PROGRAMOWANIE NISKOPOZIOMOWE. Systemy liczbowe. Pamięć PN.01. c Dr inż. Ignacy Pardyka. Rok akad. 2011/2012

Ćwiczenie nr 6. Programowanie mieszane

Informatyka I. Typy danych. Operacje arytmetyczne. Konwersje typów. Zmienne. Wczytywanie danych z klawiatury. dr hab. inż. Andrzej Czerepicki

Przedmiot : Programowanie w języku wewnętrznym. Ćwiczenie nr 4

Architektura komputerów. Asembler procesorów rodziny x86

Zuzanna Hartleb i Artur Angiel

Architektura komputerów

Instrukcja do ćwiczeń nr 4 typy i rodzaje zmiennych w języku C dla AVR, oraz ich deklarowanie, oraz podstawowe operatory

PROGRAMOWANIE NISKOPOZIOMOWE

Programowanie niskopoziomowe. dr inż. Paweł Pełczyński

/* dołączenie pliku nagłówkowego zawierającego deklaracje symboli dla wykorzystywanego mikrokontrolera */ #include <aduc834.h>

Programowanie w C++ Wykład 2. Katarzyna Grzelak. 4 marca K.Grzelak (Wykład 1) Programowanie w C++ 1 / 44

JĘZYKI PROGRAMOWANIA Z PROGRAMOWANIEM OBIEKTOWYM. Wykład 6

Architektura komputerów

PRZYDZIAŁ PAMIĘCI OPERACYJNEJ

Programowanie w asemblerze Architektury równoległe

Zadanie Zaobserwuj zachowanie procesora i stosu podczas wykonywania następujących programów

Rejestry procesora. Nazwa ilość bitów. AX 16 (accumulator) rejestr akumulatora. BX 16 (base) rejestr bazowy. CX 16 (count) rejestr licznika

Programowanie Niskopoziomowe

Język ludzki kod maszynowy

1. Wartość, jaką odczytuje się z obszaru przydzielonego obiektowi to: a) I - wartość b) definicja obiektu c) typ oboektu d) p - wartość

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Adresowanie obiektów. Adresowanie bitów. Adresowanie bajtów i słów. Adresowanie bajtów i słów. Adresowanie timerów i liczników. Adresowanie timerów

Paradygmaty programowania

Architektura Systemów Komputerowych

Programowanie w C++ Wykład 2. Katarzyna Grzelak. 5 marca K.Grzelak (Wykład 1) Programowanie w C++ 1 / 41

Instrukcja do ćwiczenia P4 Analiza semantyczna i generowanie kodu Język: Ada

Pętle i tablice. Spotkanie 3. Pętle: for, while, do while. Tablice. Przykłady

Widoczność zmiennych Czy wartości każdej zmiennej można zmieniać w dowolnym miejscu kodu? Czy można zadeklarować dwie zmienne o takich samych nazwach?

Grzegorz Cygan. Wstęp do programowania mikrosterowników w języku C

CPU ROM, RAM. Rejestry procesora. We/Wy. Cezary Bolek Uniwersytet Łódzki Wydział Zarządzania Katedra Informatyki

PROGRAMOWANIE NISKOPOZIOMOWE. Adresowanie pośrednie rejestrowe. Stos PN.04. c Dr inż. Ignacy Pardyka. Rok akad. 2011/2012

Architektura komputerów

1 Podstawy c++ w pigułce.

Jak wiemy, wszystkich danych nie zmieścimy w pamięci. A nawet jeśli zmieścimy, to pozostaną tam tylko do najbliższego wyłączenia zasilania.

2 Przygotował: mgr inż. Maciej Lasota

Lab 9 Podstawy Programowania

Programowanie Niskopoziomowe

Programowanie procesorów graficznych GPGPU

Operatory. Operatory bitowe i uzupełnienie informacji o pozostałych operatorach. Programowanie Proceduralne 1

Optymalizacja wykonania programów sekwencyjnych. Krzysztof Banaś Obliczenia Wysokiej Wydajności 1

Język C zajęcia nr 11. Funkcje

Po uruchomieniu programu nasza litera zostanie wyświetlona na ekranie

Podstawy programowania

Lista Rozkazów: Język komputera

Wskaźniki. Informatyka

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Zmienne, stałe i operatory

Wskaźniki w C. Anna Gogolińska

A Machine Architecture that is Really Intuitive and Easy. Dane: notacja dwójkowa, zapis w kodzie dopełnieniowym

Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

PMiK Programowanie Mikrokontrolera 8051

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Praktycznie całe zamieszanie dotyczące konwencji wywoływania funkcji kręci się w okół wskaźnika stosu.

Spis treœci. Co to jest mikrokontroler? Kody i liczby stosowane w systemach komputerowych. Podstawowe elementy logiczne

MOŻLIWOŚCI PROGRAMOWE MIKROPROCESORÓW

Wstęp do programowania INP003203L rok akademicki 2018/19 semestr zimowy. Laboratorium 2. Karol Tarnowski A-1 p.

Mikrokontroler ATmega32. Język symboliczny

ZASADY PROGRAMOWANIA KOMPUTERÓW

Programowanie strukturalne i obiektowe. Funkcje

Inżynieria Wytwarzania Systemów Wbudowanych

Lista instrukcji mikroprocesora Programowanie w assemblerze

START: ; start programu od adresu 0100H ; zerowanie komórek od 01H do 07FH ( 1 dec dec)

Wstęp do programowania

Logiczny model komputera i działanie procesora. Część 1.

Tablice (jedno i wielowymiarowe), łańcuchy znaków

Programowanie Niskopoziomowe

Metody Kompilacji Wykład 1 Wstęp

Tablice, funkcje - wprowadzenie

Programowanie w elektronice: Podstawy C

ARCHITEKTURA PROCESORA,

Języki i paradygmaty programowania

Programowanie C++ Wykład 2 - podstawy języka C++ dr inż. Jakub Możaryn. Warszawa, Instytut Automatyki i Robotyki

petla:... ; etykieta określa adres w pamięci kodu (docelowe miejsce skoku) DJNZ R7, petla

Dr inż. Grażyna KRUPIŃSKA. D-10 pokój 227 WYKŁAD 7 WSTĘP DO INFORMATYKI

Wstęp do programowania. Reprezentacje liczb. Liczby naturalne, całkowite i rzeczywiste w układzie binarnym

PROGRAMOWANIE NISKOPOZIOMOWE. Struktury w C. Przykład struktury PN.06. c Dr inż. Ignacy Pardyka. Rok akad. 2011/2012

Zaawansowane programowanie w języku C++ Zarządzanie pamięcią w C++

Część 4 życie programu

Transkrypt:

Programowanie w asemblerze Optymalizacja 17 stycznia 2017

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

Przesłania warunkowe: przykład Znalezienie większej z dwóch liczb (w EAX i EBX, wynik w ECX): mov ecx,eax cmp ebx,ecx cmova ecx,ebx

Przesłania warunkowe: błędy 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 ;Być może zwrócimy zero test rdi,rdi ;xp == 0? cmovne eax,[rdi] ;Być może zwrócimy *xp Ale wtedy dereferencja xp nastapi zawsze (nawet dla wskaźnika NULL), a tego chcemy uniknać.

Unikanie skoków Unikanie skoków to szerszy problem. Popatrzmy na obliczenie wartości bezwzględnej omiń: test eax,eax jns omiń neg eax ;Ustawmy flagi ;znak dodatni

Unikanie skoków Można to zrobić inaczej: mov ecx,eax sar ecx,31 ;wszędzie bit znaku xor eax,ecx ;odwracamy bity sub eax,ecx ;odejmujemy -1 i mamy uzupełnienie do 2

Potęga 2 Kolejna sztuczka: jak sprawdzić, czy liczba w EAX jest potęga dwójki? mov ebx,eax ;albo lea ebx,[eax - 1] dec ebx test eax,ebx jnz niejest

Podpowiedzi Procesor próbuje zgadywać, czy skok warunkowy będzie wykonany. Przy statycznym przewidywaniu zakłada, że skok do tyłu będzie 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... L9: ;HT = będzie skok

Podpowiedzi Często nie warto trzymać danych w pamięci buforowej, jeśli używane jednorazowo Instrukcje zapisu bezpośredniego (non-temporal store) MOVNTI, MOVNTPD itp. podczas zapisu do pamięci omijaja cache.

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.

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, więc trzeba przełaczać tryb w koprocesorze.

Konserwatywność kompilatora Inny kandydat do Oscara to pointer aliasing. W poniższym kodzie kompilator nie wyciagnie obliczenia *p + 2 przed pętlę 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]); }

Konserwatywność kompilatora Czasem recepty sa proste. Poniższy kod dwukrotnie pobiera arg1->p1 z pamięci: 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 pamięci. A wystarczy wprowadzić zmienna lokalna i przypisać na nia S1->p1.

Asembler Asembler pozwala korzystać z dostępu do usług niskiego poziomu: Rejestry i bezpośrednie wejście/wyjście Omijanie konwencji kompilatora: inne przekazywanie parametrów, naruszanie zasad przydziału pamięci, iteracyjne wołanie procedur Łaczenie niezgodnych fragmentów kodu, np. zbudowanych przez inne kompilatory Ręczna optymalizacja kodu w celu dopasowania do bardzo konkretnej konfiguracji sprzętowej

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ć następujaco movaps xmm0,[a] maxps xmm0,[b] movaps [c],xmm0 ;Load a vector ;max(a,b) ;c = a > b? a : b

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 się w asemblerze jako LDRB Relem, [Rtab, Rindprz, LSR#16] ADD Rindprz, Rindprz, Rindprz, LSL#16

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 pamięć RAM i dyskowa były małe i kosztowne. Ale CISC o dziwo ma także zalety. Zwięzłość kodu dobrze pasuje do wymogów pamięci buforowych (cache) o ograniczonych rozmiarach. Główny problem procesorów x86 to mała liczba rejestrów, trochę poprawiony przy projektowaniu x86-64.

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ętu).

Kod 64-bitowy Zalety: Więcej rejestrów: nie trzeba trzymać zmiennych i wyników pośrednich w pamięcu 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 pamięci. Wbudowany repertuar SSE2. Względna adresacja danych, wydajny kod relokowalny.

Kod 64-bitowy Wady: Dwa razy większe adresy i pozycje stosu: kłopoty z pamięcia buforowa. Dostęp do statycznych i globalnych tablic wymaga więcej instrukcji dla dużych obrazów pamięci. Dotyczy głównie Windows i Maca. Bardziej skomplikowane obliczanie adresu gdy rozmiar większy niż 2GB. Niektóre instrukcje dłuższe.

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 klasę wektorów i przeciażyć dla niej operator +. Funkcje intrinsic występuja w kompilatorach Microsoft, Intela i GNU.

Ogladanie kodu z kompilatora Różne powody: Sprawdzanie, czy nie widać wyraźnych miejsc do ręcznego 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ęsto najwięcej kłopotów. A czasem wykryjemy bład w kompilatorze

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.

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.

Ograniczenia sprzętowe Na ARM rejestry sa 32-bitowe. Należy unikać typów char i short dla liczników pętli, bo trzeba w kodzie ręcznie 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, więc 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.

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 się 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);

Zależności Czasem wyglada to dziwnie, na przykład instrukcję przypisania y = a + b + c + d; warto zastapić przez y = (a + b) + (c + d); Specyfikacja wielu języków programowania nakłada wymóg wykonywania od lewej do prawej (np. żeby błędy zaokragleń były zawsze takie same) i kompilator nic wtedy nie może zrobić.

Rejestry częściowe Niektóre CPU robia out of order execution ale nie sa w stanie przemianować rejestrów częś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 Jeśli zastapimy tę instrukcję przez movzx eax,[mem3] ;operandy 16-bitowe to zależność zostaje zlikwidowana. Pewnie dlatego w trybie 64-bitowym dzieje się to automatycznie przy przesłaniach 32-bitowych.

Zmiany kolejności Głównie na mocno potokowanych RISCach (np. ARM), wymuszone specyfika procesora Na ARM9TDMI dla instrukcji ładowania z pamięci (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 pamięci i zapisywania do niej działaja w tle. Pozornie więc 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.

Skoki i procedury Pobieranie kodu po (nieoczekiwanym) skoku generuje opóźnienia rzędu 1-3 cykli. Największe, 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 się przez skoki!

Metaprogramowanie Zamiast pisać pokrętne makra asemblera albo nadużywać m4 lepiej pisać programy, które generuja inne programy lub ich części: Generatory tablic sinusów, cosinusów albo lat przestępnych 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 języku i dodatkowych ograniczeń.

Tuning: narzędzia AMD Code Analyst Intel VTune New-Jersey Machine-Code Toolkit (w ML) http://www.eecs.harvard.edu/ nr/toolkit/