Kurs programowania w asemblerze


Optymalizacja w asemblerze

Na tej stronie zajmiemy się optymalizacją kodu w Asemblerze. Zastanowimy się jak przyspieszyć działanie naszego programu i jak zmniejszyć jego objętość.

Strona jest podzielona na:

Do optymalizacji kodu przyda się również tabela czasu wykonania poszczególnych instrukcji procesora.

Szybkość

Programy napisane w Asemblerze z reguły są bardzo szybkie. Na pewno szybsze niż te napisane w BASIC-u, czy Pascal-u. Czasami zachodzi jednak potrzeba stworzenia programu nadzwyczaj szybkiego. Gdy np. chcemy stworzyć jakąś animację w wysokiej rozdzielczości, z teksturowaniem szybkość naszego kodu ma ogromne znaczenie.

Na początek kilka ogólnych reguł:

  • Procesor 386
    1. zawsze kiedy jest możliwość wyrównuj kod i dane do granicy podwójnego słowa
    2. adresowanie 32-bitowe w kodzie 16-bitowym powoduje opóźnienie
    3. używaj 32-bitowy tryb adresowania do dużych segmentów
  • Procesor 486
    1. często wykonywane procedury i adresy skoków powinny być wyrównane do granicy 16 bajtów
    2. dane zawsze wyrównuj do granicy zgodnej z wielkością danej (słowa do granicy słowa)
    3. zamiast stosowania wielokrotnego przesuwania (obrotu) bitów o jeden przesuń o od razu więcej (już można)

1. Rejestry

Podczas programowania często zachodzi potrzeba zapamiętania jakiejś wartości. Wówczas niektórzy z nas tworzą sobie zmienne. Oprócz tego, że te zmienne zajmują pewną pamięć, odczytanie z nich wartości jest trudniejsze i stwarza możliwość popełnienia błędu, operacje na nich są znacznie wolniejsze, niż na rejestrach procesora. Rejestry są najszybsze w naszym komputerze, dlatego wykorzystujmy je w 100%. Jeśli jakaś liczba ma zostać zapamiętana na krótką chwilę, a nie mamy wolnych rejestrów, to użyjmy stosu. Jest on również szybszy od zmiennych. Róbmy więc zmiennych jak najmniej.

2. Makrodefinicje

Jak działają makrodefinicje nie trzeba chyba tłumaczyć. Wstawiają w odpowiednie miejsce, odpowiedni kod. Stosujmy więc makrodefinicję wszędzie tam, gdzie zależy nam na prędkości. Używanie procedur jest wygodne, jednak wywołanie procedury zajmuje wiele czasu. Położenie na stos odpowiednich rejestrów i wykonanie bardzo powolnego skoku, to dla nas za dużo. Jeśli więc użyjemy makrodefinicji, nie wykonamy żadnego skoku. Program jest czytelny i szybki. Jedyna wada, to objętość, która rośnie w zadziwiająco szybkim tempie.

3. Pętle

Istnieje taka instrukcja w Asemblerze, która się nazywa LOOP. Służy ona do zmniejszania licznika CX i wykonywania skoku, dopóty dopóki CX<>0. Jest ona bardzo wolna, zajmuje na 486 6/7 cykli, a na PENTIUM 5NP. Można czasami pętle znacznie przyspieszyć, przykładowo mamy napisać program, który wykona pętlę pięć razy i za każdym razem zwiększy rejestr AX o jeden. Normalnie napisalibyśmy tak:

MOV CX,5
ET1:
INC AX
LOOP ET1

Można go przyspieszyć pisząc tak:

MOV CX,5
ET1:
INC AX
DEC CX
JNZ ET1

Dlaczego drugi sposób jest szybszy? Bo dodatkowa instrukcja DEC (na 486 i PENTIUM) trwa 1 cykl, a JNZ na 486 trwa 1/3 cykli, a na PENTIUM 1V, co oznacza całkowity czas na 486 max. 4 cykle (poprzednie rozwiązanie 7), a na PENTIUM 1 cykl, bo za jednym razem zostanie wykonana instrukcja DEC i JNZ (podczas gdy LOOP 5NP).

4. Koprocesor

Do skomplikowanych operacji używajmy koprocesora arytmetycznego. Wykonuje on obliczenia na liczbach zmiennoprzecinkowych znacznie szybciej, niż procesor. Opis programowania koprocesora zamieściłem w oddzielnym artykule.

Objętość

Optymalizacja pod względem objętości jest również bardzo ważna. Czasami zachodzi potrzeba napisania programu, który nie ważne ile będzie wykonywał daną operację, ważne by działał i był mały. Zazwyczaj jednak programy małe są i szybkie, ale nie zawsze.

1. Procedury

Jednym ze sposobów zmniejszenia objętości programu jest stosowanie procedur. Jeśli jakiś fragment programu jest wykonywany wiele razy można zrobić z niego procedurę i w razie potrzeby wykonywać tylko skok. Ten sposób znacznie zmniejszy objętość naszego kodu, jednak skoki są powolne, przez co szybkość programu spadnie.

2. Stos

Stos jest bardzo poręcznym narzędziem. Zapamiętanie dowolnego rejestru 16 bitowego na stosie zajmuje jeden bajt, 32 bitowego dwa bajty, a zapamiętanie rejestru 16 bitowego w zmiennej zajmuje 3 bajty, a 32 bitowego 4 bajty. Jeśli więc mamy do zapamiętanie jakąś zmienną, na krótki czas, to jak się da to stosujmy stos. Skracamy kod i nie ma konieczności tworzenia zmiennych. Mamy również mniejsze ryzyko wystąpienia błędu.

3. Segmenty

Wiemy, że do rejestrów segmentowych nie wolno zapamiętywać wartości stałych, np. konkretnej liczby. Trzeba to uczynić korzystając np. z rejestru. Zapamiętujemy wówczas liczbę w rejestrze i ten przesyłamy do rejestru segmentowego. Przykładowy program może wyglądać tak:

MOV AX,0A000H
MOV DS,AX

Jest na to wolniejszy (o jeden cykl), ale mniejszy objętościowo sposób. Można wykorzystać tą właściwość stosu i instrukcji PUSH, że można kłaść wartość stałą na stos i to, że można zdjąć coś ze stosu i zapamiętać w rejestrze segmentowym. Nasz program z wykorzystaniem stosu wyglądać może tak:

PUSH 0A000H
POP DS

Sposób pierwszy zajmuje 5 bajtów (2 cykle), natomiast drugi 4 bajty (3 cykle).

4. Zerowanie

Bardzo często podczas programowania zachodzi potrzeba wyzerowania jakiegoś rejestru. Wówczas niektórzy piszą:

MOV AX,0

Jest to niepotrzebne zwiększanie objętości programu. Dużo lepszym sposobem jest zastosowanie instrukcji logicznej XOR lub AND. W XOR wykorzystujemy tą właściwość, że jeżeli para bitów jest taka sama (równa 0 lub 1), to wynik jest 0. Jeśli więc ksorujemy liczbę przez samą siebie, to wszystkie odpowiadające sobie bity są identyczne, więc wynik jest równy 0. Używając AND wykorzystujemy tą właściwość, że jeżeli pomnożymy logicznie dowolny bit przez 0, to wynik będzie 0. Dlatego wystarczy pomnożyć liczbę przez 000000B, czyli 0. Powinniśmy więc napisać:

XOR AX,AX lub
AND AX,0

Obie te instrukcje zajmują dwa bajty, a instrukcja MOV trzy.

Przy programowaniu 32 bitowym:

MOV EAX,0 ; 5 bajtów
xor eax,eax ; 2 bajty

Zamienniki

Poniżej w tabeli wypisałem różne komendy oraz odpowiedniki, na które warto je zamienić (wykonają to samo, ale szybciej lub zajmą mniej!)

KomendaZamiennikOpis
cwdmov ah,al
sar ah,7
od 386
cdqmov edx,eax
sar edx,31
od 486
cmp mem,regcmp reg,memna 286 szybsze o 1 cykl
cmp reg,memcmp mem,regna 386 szybsze o 1 cykl
lodsbmov al,[si]
inc si
od 486
lodswmov ax,[si]
add si,2
od 486
lodsdmov eax,[si]
add si,4
od 486
loopdec (s)cx
jnz etykieta
od 386
movsbmov al,[si]
inc si
mov [di],al
inc di
od 486
movswmov ax,[si]
add si,2
mov [di],ax
add di,2
od 486
movsdmov eax,[esi]
add esi,4
mov [edi],eax
add edi,4
od 486
pop mempop reg
mov mem,reg
od 486
push memmov reg,mem
push reg
od 486
shl reg,1add reg,regod Pentium

Czas wykonania - strona z czasami wykonania niektórych instrukcji
Intel - Dokładny opis wszystkich instrukcji procesora (w tym czasy wykonania) 23.KB
PentOpt - programy Michaela L. Schmit'a obliczające, ile czasu wykonuje się dany program




Autor: Karol Wierzchołowski, opracowano: 12.01.2002 r. Wszelkie prawa zastrzeżone.
Kontakt

Jeśli chcesz się z nami skontaktować napisz na adres: info(at)binboy.org lub odwiedź nasz profil na Facebooku!

O Nas

Serwis binboy.org to kopalnia wiedzy dla wszystkich z branży IT, w szczególności dla programistów i webmasterów. To duży zbiór kursów programowania, tutoriali, darmowych ebooków, setki kodów źródłowych itp.

Bądź w kontakcie

Panel użytkownika

Zaloguj się do panelu użytkownika.
Nie masz konta? Zarejestruj się!
Zapomniałeś hasła?