Jak zapewne większość wie, koprocesor (FPU = Floating Point Unit, NPX = Numerical Processor eXtension) służy do wykonywania działań matematycznych. Podstawowy procesor też oczywiście wykonuje działania matematyczne (dodawanie, mnożenie, ...) ale tylko na liczbach całkowitych. Z czasem jednak przyszła potrzeba wykonywania obliczeń na liczbach niecałkowitych, czyli ułamkach (liczbach zmiennoprzecinkowych). Dlatego firmy produkujące procesory zaczęły je wyposażać właśnie w układy wspomagające pracę na ułamkach. Do procesorów 8086, 80286, 80386 były dołączane jako osobne układy koprocesory: 8087, 80287, 80387 (80187 nie wprowadził żadnych istotnych nowości. Była to przeróbka 8087, a może nawet był to po prostu ten sam układ). Procesory 486SX miały jeszcze oddzielny koprocesor (80387) a od 486DX (w każdym razie u Intela) koprocesor był już na jednym chipie z procesorem. I tak jest do dziś.
Ale dość wstępu. Pora na szczegóły.
Zanim zaczniemy cokolwiek robić, trzeba wiedzieć, na czym
ten cały koprocesor operuje.
Oprócz liczb całkowitych, FPU operuje na liczbach ułamkowych różnej precyzji:
float
.double
long double
Jak widać, ilości bitów są oczywiście skończone. Więc nie każdą liczbę rzeczywistą da się dokładnie zapisać w postaci binarnej. Na przykład, jedna dziesiąta (0.1) zapisana dwójkowo jest ułamkiem nieskończonym okresowym! Poza tym, to, czego nas uczyli na matematyce, na przykład oczywista równość: a+(b-a)=b nie musi być prawdą w świecie ułamków w procesorze ze względu na brak precyzji!
Poza ułamkami, FPU umie operować na BCD (binary coded decimal). W takich liczbach 1 bajt dzieli się na 2 części po 4 bity, z których każda może mieć wartość 0-9. Cały bajt reprezentuje więc liczby od 0 do 99, w których cyfra jedności jest w młodszych 4 bitach a cyfra dziesiątek - w starszych 4 bitach.
Szczegółami zapisu liczb ułamkowych nie będziemy się tutaj zajmować. Polecam, tak jak poprzednio: strony Intela i strony AMD, gdzie znajduje się też kompletny opis wszystkich instrukcji procesora i koprocesora.
Po zapoznaniu się z typami (a przede wszystkim z rozmiarami) liczb ułamkowych, powstaje pytanie:
gdzie koprocesor przechowuje takie ilości danych?
FPU ma specjalnie do tego celu przeznaczonych 8 rejestrów, po 80 bitów każdy. W operacjach
wewnętrznych (bez pobierania lub zapisywania danych do pamięci) FPU zawsze używa
rozszerzonej precyzji.
Rejestry danych nazwano st(0), st(1), ... , st(7) (NASM: st0 ... st7). Nie działają jednak one tak, jak zwykłe rejestry, lecz jak ... stos! To znaczy, że dowolnie dostępna jest tylko wartość ostatnio położona na stosie czyli wierzchołek stosu, w tym przypadku: st(0). Znaczy to, że do pamięci (FPU operuje tylko na własnych rejestrach lub pamięci - nie może używać rejestrów ogólnego przeznaczenia na przykład EAX itp.) może być zapisana tylko wartość z st(0), a każda wartość pobierana z pamięci idzie do st(0) a stare st(0) przesuwa się do st(1) itd. każdy rejestr przesuwa się o 1 dalej. Jeżeli brakuje na to miejsca, to FPU może wygenerować przerwanie (wyjątek) a rejestry danych będą prawdopodobnie zawierać śmieci.
Operowanie na rejestrach FPU będzie wymagało nieco więcej uwagi niż na zwykłych, ale i do tego można się przyzwyczaić.
Zacznijmy od omówienia kilku podstawowych instrukcji. Przez [mem] będę nazywał dane będące
w pamięci (32-, 64- lub 80-bitowe, int
oznacza liczbę całkowitą), st
jest częstym skrótem
od st(0)
. Jeżeli komenda kończy się na P to oznacza, że zdejmuje dane raz ze stosu, PP
oznacza, że zdejmuje 2 razy: st(0) i st(1).
FLD/FILD [mem]
- załaduj liczbę rzeczywistą/całkowitą z pamięci. Dla liczby rzeczywistej
jest to 32, 64 lub 80 bitów. Dla całkowitej - 16, 32 lub 64 bity.FST [mem32/64/80]
- do pamięci idzie liczba ze st(0).FSTP [mem32/64/80]
- zapisz st(0) w pamięci i zdejmij je ze stosu. Znaczy
to tyle, że st(1) o ile istnieje, staje się st(0) itd. każdy rejestr cofa się o 1.FIST [mem16/32]
- ewentualnie obciętą do całkowitej liczbę z st(0) zapisz do pamięci.FISTP [mem16/32/64]
- jak wyżej, tylko ze zdjęciem ze stosu.FXCH st(i)
- zamień st(0) z st(i).FLDZ
- załaduj zero. st(0) = 0.0FLD1
- załaduj 1. st(0) = 1.0FLDPI
- załaduj pi.FLDL2T
- załaduj log2(10)FLDL2E
- załaduj log2(e)FLDLG2
- załaduj log(2)=log10(2)FLDLN2
- załaduj ln(2)FADD
, składnia identyczna jak w odejmowaniu prostymFSUB [mem32/64]
st := st-[mem] FSUB st(0),st(i)
st := st-st(i)FSUB st(i),st(0)
st(i) := st(i)-st(0)FSUBP st(i), st(0)
st(i) := st(i)-st(0) i zdejmijFSUBP
(bez argumentów) = FSUBP st(1),st(0)
FISUB [mem16/32int]
st := st-[mem]FSUBR [mem32/64]
st := [mem]-st(0)FSUBR st(0),st(i)
st := st(i)-st(0)FSUBR st(i),st(0)
st(i) := st(0)-st(i)FSUBRP st(i),st(0)
st(i) := st(0)-st(i) i zdejmijFSUBRP
(bez argumentów) = FSUBRP st(1),st(0)
FISUBR [mem16/32int]
st := [mem]-stFMUL
, składnia identyczna jak w odejmowaniu prostym.FDIV
, składnia identyczna jak w odejmowaniu prostym.FDIVR
, składnia identyczna jak w odejmowaniu odwrotnym.FABS
(bez argumentów) zastępuje st(0) jego wartością bezwzględną.FCHS
: st(0) := -st(0).FSQRT
: st(0) := SQRT[ st(0) ]FPREM
, FPREM1
st(0) := st(0) mod st(1).FRNDINT
: st(0) := (int)st(0).FCOM/FCOMP/FCOMPP
, FUCOM/FUCOMP/FUCOMPP
, FICOM/FICOMP
,
FCOMI/FCOMIP
, FUCOMI/FUCOMIP
, FTST
, FXAM
.Tutaj trzeba trochę omówić sytuację. FPU oprócz rejestrów danych zawiera także rejestr
kontrolny (16 bitów) i rejestr stanu (16 bitów).
W rejestrze stanu są 4 bity nazwane
C0, C1, C2 i C3. To one wskazują wynik ostatniego porównania, a układ ich jest taki sam,
jak flag procesora, co pozwala na ich szybkie przeniesienie do flag procesora.
Aby odczytać wynik porównania, należy zrobić takie coś:
fcom fstsw ax ; tylko od 386. Inaczej: ; fstsw word ptr [zmienna] / mov ax,[zmienna] sahf ; AH -> flagi
i używać normalnych komend JE, JB itp.
FCOM st(n)/[mem]
- porównaj st(0) z st(n)
(lub zmienną w pamięci) bez zdejmowania st(0) ze stosu FPUFCOMP st(n)/[mem]
- porównaj st(0) z st(n)
(lub zmienną w pamięci) i zdejmij st(0)FCOMPP
- porównaj st(0) z st(1) i
zdejmij oba ze stosuFICOM [mem]
- porównaj st(0) ze
zmienną całkowitą 16- lub 32-bitową w pamięciFICOMP [mem]
- porównaj st(0) ze
zmienną całkowitą 16- lub 32-bitową w pamięci, zdejmij st(0)FCOMI st(0), st(n)
- porównaj st(0) z st(n) i ustaw flagi
procesora, nie tylko FPUFCOMIP st(0), st(n)
- porównaj st(0) z st(n) i ustaw flagi
procesora, nie tylko FPU, zdejmij st(0)Komendy kończące się na I lub IP zapisują swój wynik bezpośrednio do flag procesora. Można tych
flag od razu używać (JZ
, JA
, ...). Te komendy są dostępne tylko od 386.
FTST
porównuje st(0) z zerem.
FXAM
bada, co jest w st(0) - prawidłowa liczba, błąd (NaN =
Not a Number), czy 0.
FSIN
- st(0) := sinus [st(0)]FCOS
- st(0) := kosinus [st(0)]FSINCOS
- st(0) := kosinus [st(0)], st(1) := sinus [st(0)]FPTAN
- partial tangent = tangens st(0) := tg [st(0)]FPATAN
- arcus tangens st(0) := arctg [st(0)]FYL2X
st(1) := st(1)*log2[st(0)] i zdejmijFYL2XPI
st(1) := st(1)*log2[ st(0) + 1.0 ] i zdejmij F2XM1
st(0) := 2^[st(0)] - 1FINIT/FNINIT
- inicjalizacja FPU. Litera N po F oznacza,
aby nie brać po uwagę potencjalnych niezałatwionych wyjątków.FLDCW
, FSTCW/FNSTCW
- Load/Store control word
- zapisuje 16 kontrolnych bitów do pamięci,
gdzie można je zmieniać na przykład aby zmienić sposób zaokrąglania liczb.FSTSW/FNSTSW
- zapisz do pamięci (lub rejestru AX) słowo statusu, czyli stan FPUFCLEX/FNCLEX
- wyczyść wyjątkiFLDENV
, FSTENV/FNSTENV
- wczytaj/zapisz środowisko (rejestry stanu,
kontrolny i kilka innych, bez rejestrów danych). Wymaga 14 albo 28 bajtów pamięci, w zależności
od trybu pracy procesora (rzeczywisty - DOS lub chroniony - Windows/Linux).FRSTOR
, FSAVE/FNSAVE
- jak wyżej, tylko że z rejestrami danych. Wymaga 94
lub 108 bajtów w pamięci, zależnie od trybu procesora.FINCSTP
, FDECSTP
- zwiększ/zmniejsz wskaźnik stosu - przesuń st(0)
na st(7), st(1) na st(0) itd. oraz w drugą stronę, odpowiednio.FFREE
- zwolnij podany rejestr danychFNOP
- no operation. Nic nie robi, ale zabiera czas.WAIT/FWAIT
- czekaj, aż FPU skończy pracę. Używane do synchronizacji z CPU.Dość już teorii, pora na przykłady. Programiki te wymyśliłem pisząc ten kurs.
Przykład 1 (będzie to program typu .exe, bo dodamy moją bibliotekę do wyświetlania wyników):
; TASM: ; z wyświetlaniem: ; tasm naszplik.asm ; tlink naszplik.obj bibl\lib\bibldos.lib ; bez wyświetlania: ; tasm naszplik.asm ; tlink naszplik.obj .model small .stack 400h ; stos dla programu .exe .data dzielna DQ 1234DDh ; 4013 91a6 e800 0000 0000 dzielnik DQ 10000h iloraz DT ? .code ; jeśli nie chcesz wyświetlania, usuń tę linijkę niżej: include incl\std_bibl.inc start: mov ax, @data mov ds, ax mov es, ax ; konieczne w programie typu .exe ! ; Życie przestaje być ; wygodne. DOS już nam nie ustawi ; DS=ES=CS. A nasze dane są w ; segmencie kodu, stad ustawiamy DS=CS ; W programie typu .com to na pewno ; nie zaszkodzi. finit ; zawsze o tym pamiętaj ! fild qword ptr [dzielna] ; ładujemy dzielną. st(0) = 1234DD fild qword ptr [dzielnik] ; ładujemy dzielnik. st(0) = 10000h, ; st(1) = 1234DD fdivp ; dzielimy. st(1) := st(1)/st(0) i ; zdejmij. st(0) ~= 18.2 fstp tbyte ptr [iloraz] ; zapisujemy st(0) do pamięci i ; zdejmujemy ze stosu ; jeśli nie chcesz wyświetlania, usuń te 3 linijki niżej: mov di, offset iloraz ; DI=adres zmiennej zawierającej wynik piszd80 ; wyświetl wynik nwln ; przejdź do nowej linii mov ax, 4c00h int 21h end start
Teraz wersja dla NASMa. O tym, jak NASMem zrobić program typu .exe napisane jest w jego dokumentacji. Wymaga to przede wszystkim stworzenia własnego segmentu stosu i nakierowanie na niego rejestrów SS:SP. Trzeba mieć też odpowiedni linker, na przykład VAL. Można jednak użyć jednego z plików dołączonych do mojej biblioteki i tak też zrobimy.
; NASM: ; z wyświetlaniem: ; nasm -f obj -o fpu1.obj fpu1.asm ; val fpu1.obj,fpu1.exe,,bibl\lib\bibldos.lib, ; bez wyświetlania: ; nasm -f obj -o fpu1.obj fpu1.asm ; val fpu1.obj,fpu1.exe,,, ; częściowa zgodność z TASMem: %include "bibl\incl\dosbios\nasm\do_nasma.inc" ; jeśli nie chcesz wyświetlania, usuń tę linijkę niżej: %include "bibl\incl\dosbios\nasm\std_bibl.inc" .model small .stack 400h .code ..start: mov ax, cs mov ds, ax mov es, ax ; konieczne w programie typu .exe ! ; Życie przestaje być ; wygodne. DOS już nam nie ustawi DS=ES=CS. ; A nasze dane są w ; segmencie kodu, stąd ustawiamy DS=CS. ; W programie typu .com to na pewno nie ; zaszkodzi. finit ; zawsze o tym pamiętaj ! fild dword [dzielna] ; ładujemy dzielną. st(0) = 1234DD fild dword [dzielnik]; ładujemy dzielnik. st(0) = 10000h, ; st(1) = 1234DD fdivp st1, st0 ; dzielimy. st(1) := st(1)/st(0) i ; zdejmij. st(0) ~= 18.2 ; FASM: fdivp st(1) fstp tword [iloraz] ; zapisujemy st(0) do pamięci i zdejmujemy ; ze stosu ; jeśli nie chcesz wyświetlania, usuń te 3 linijki niżej: mov di, iloraz piszd80 ; wyświetl wynik nwln ; przejdź do nowego wiersza mov ax, 4c00h int 21h align 8 ; NASM w tym miejscu dorobi kilka NOPów ; (instrukcji nic nie robiących, ale ; zabierających czas), aby adres dzielił się ; przez 8 (patrz dalej). dzielna dd 1234ddh ; 4013 91a6 e800 0000 0000 dzielnik dd 10000h iloraz dt 0.0
; FASM: ; z wyświetlaniem (stary format biblioteki - OMF): ; fasm fpu1.asm fpu1.obj ; alink fpu1.obj bibl\lib\bibldos.lib -c- -entry _start -oEXE -m- ; z wyświetlaniem (nowy format biblioteki - COFF): ; fasm fpu1.asm fpu1.obj ; ld -s -o fpu1.exe fpu1.obj bibl\lib\bibldos.a ; bez wyświetlania: ; fasm fpu1.asm fpu1.exe ; jeśli chcesz wyświetlanie (stary format biblioteki - OMF): format coff public _start public start include "bibl\incl\dosbios\fasm\std_bibl.inc" use16 ; jeśli chcesz wyświetlanie (nowy format biblioteki - COFF): ;format coff ;public _start ;public start ;include "bibl\incl\dosbios\fasm32\std_bibl.inc" ; jeśli nie chcesz wyświetlania: ;format MZ ;entry kod:_start ;stack 400h ;segment kod start: _start: ; wyłączyć trzy poniższe linie w przypadku FASMa z nowym formatem ; biblioteki (32-bitowy COFF nie pozwala na manipulację segmentami) mov ax, cs mov ds, ax mov es, ax ; konieczne w programie typu .exe ! ; Życie przestaje być ; wygodne. DOS już nam nie ustawi DS=ES=CS. ; A nasze dane są w ; segmencie kodu, stąd ustawiamy DS=CS. ; W programie typu .com to na pewno nie ; zaszkodzi. finit ; zawsze o tym pamiętaj ! fild dword [dzielna] ; ładujemy dzielną. st(0) = 1234DD fild dword [dzielnik]; ładujemy dzielnik. st(0) = 10000h, ; st(1) = 1234DD fdivp ; dzielimy. st(1) := st(1)/st(0) i ; zdejmij. st(0) ~= 18.2 fstp tword [iloraz] ; zapisujemy st(0) do pamięci i zdejmujemy ; ze stosu ; jeśli nie chcesz wyświetlania, usuń te 3 linijki niżej: mov edi, iloraz piszd80 ; wyświetl wynik nwln ; przejdź do nowego wiersza mov ax, 4c00h int 21h dzielna dd 1234ddh ; 4013 91a6 e800 0000 0000 dzielnik dd 10000h iloraz dt 0.0
Ten przykład do zmiennej iloraz
wstawia częstotliwość zegara
komputerowego (ok. 18,2 Hz).
Należy zwrócić uwagę na zaznaczanie rozmiarów zmiennych (dword/qword/tbyte ptr).
Dyrektywa ALIGN
ustawia kolejną zmienną/etykietę tak, że jej adres dzieli się przez 8 (qword = 8 bajtów). Dzięki
temu, operacje na pamięci są szybsze (na przykład dla zmiennej 8-bajtowej zamiast 3 razy pobierać po 4 bajty,
bo akurat tak się zdarzyło, że miała jakiś nieparzysty adres, pobiera się 2x4 bajty). Rzecz jasna, skoro
zmienna dzielna
(i dzielnik
) ma 4 bajty, to adresy zmiennych
dzielnik
i iloraz
też będą podzielne przez 4.
Ciąg cyfr po średniku to ułamkowa reprezentacja dzielnej. Skomplikowane,
prawda? Dlatego nie chciałem tego omawiać.
Przykład 2: czy sinus liczby pi rzeczywiście jest równy 0 (w komputerze)?
.model tiny .code ;.386 ; odkomentować, jeżeli .387 sprawia problemy .387 org 100h start: finit ; zawsze o tym pamiętaj ! fldpi ; wczytujemy PI fsin ; obliczamy sin(PI) ftst ; porównujemy st(0) z zerem. fstsw ax ; zapisujemy rejestr stanu bezpośrednio w AX. ; Dlatego było .387 sahf ; AH idzie do flag mov ah,9 ; AH=9, flagi niezmienione je jest_zero ; st(0) = 0? Jeśli tak, to wypisz, że jest mov dx,offset nie_zero ; zmienić DX na EDX, jeżeli sprawia problemy jmp short pisz jest_zero: mov dx,offset zero ; DX/EDX jak wyżej pisz: int 21h ; wypisz jedną z wiadomości. mov ax,4c00h int 21h nie_zero db "Sin(PI) != 0.$" zero db "Sin(PI) = 0$" end start
org 100h start: finit ; zawsze o tym pamiętaj ! fldpi ; wczytujemy PI fsin ; obliczamy sin(PI) ftst ; porównujemy st(0) z zerem. fstsw ax ; zapisujemy rejestr stanu bezpośrednio w AX. ; Dlatego było .387 sahf ; AH idzie do flag mov ah,9 ; AH=9, flagi niezmienione je jest_zero ; st(0) = 0? Jeśli tak, to wypisz, że jest mov dx,nie_zero jmp short pisz jest_zero: mov dx,zero pisz: int 21h ; wypisz jedną z wiadomości. mov ax,4c00h int 21h nie_zero db "Sin(PI) != 0$" zero db "Sin(PI) = 0$"
Przykład 3: czy pierwiastek z 256 rzeczywiście jest równy 16, czy 200 jest kwadratem liczby całkowitej (komentarze do .386/.387 jak wyżej)?
.model tiny .code .386 .387 org 100h ; program typu .com start: finit ; zawsze o tym pamiętaj ! mov ax,cs mov ds,ax ; konieczne w programie typu .exe ! ; Życie przestaje być ; wygodne. DOS już nam nie ustawi DS=ES=CS. ; A nasze dane są w ; segmencie kodu, stąd ustawiamy DS=CS. fild word ptr [dwa_pie_sze] ; st(0) = 256 fsqrt ; st(0) = sqrt(256) fild word ptr [szesnascie] ; st(0) = 16, st(1) = sqrt(256) fcompp ; porównaj st(0) i st(1), zdejmij oba ; st: [pusty] fstsw ax sahf mov ah,9 je tak256 mov dx,offset nie_256 jmp short pisz_256 tak256: mov dx,offset tak_256 pisz_256: int 21h ; wypisz stosowną wiadomość ; do zapisu stanu stosu, czyli rejestrów danych FPU ; można używać takiego schematu zapisu, ; który jest krótszy: ; st: (0), (1), (2), ... , (7) fild word ptr [dwiescie] ; st: 200 fsqrt ; st: sqrt(200) fld st(0) ; do st(0) wczytaj st(0). ; st: sqrt(200), sqrt(200) frndint ; zaokrąglij do liczby całkowitej. ; st: (int)sqrt(200), sqrt(200) fcompp ; porównaj i zdejmij oba. fstsw ax sahf mov ah,9 je tak200 mov dx,offset nie_200 jmp short pisz_200 tak200: mov dx,offset tak_200 pisz_200: int 21h ; wypisz stosowną wiadomość mov ax,4c00h int 21h dwa_pie_sze dw 256 dwiescie dw 200 szesnascie dw 16 nie_256 db "SQRT(256) != 16$" tak_256 db "SQRT(256) = 16$" nie_200 db "Liczba 200 nie jest kwadratem liczby calkowitej$" tak_200 db "Liczba 200 jest kwadratem liczby calkowitej$" end start
org 100h ; program typu .com start: finit ; zawsze o tym pamiętaj ! mov ax,cs mov ds,ax ; konieczne w programie typu .exe ! ; Życie przestaje być ; wygodne. DOS już nam nie ustawi DS=ES=CS. ; A nasze dane są w ; segmencie kodu, stąd ustawiamy DS=CS. ; W programie typu .com to na pewno nie ; zaszkodzi. fild word [dwa_pie_sze] ; st(0) = 256 fsqrt ; st(0) = sqrt(256) fild word [szesnascie] ; st(0) = 16, st(1) = sqrt(256) fcompp ; porównaj st(0) i st(1) i zdejmij oba ; st: [pusty] fstsw ax sahf mov ah,9 je tak256 mov dx, nie_256 jmp short pisz_256 tak256: mov dx,tak_256 pisz_256: int 21h ; wypisz stosowną wiadomość ; do zapisu stanu stosu, czyli rejestrów danych FPU ; można używać takiego schematu zapisu, ; który jest krótszy: ; st: (0), (1), (2), ... , (7) fild word [dwiescie] ; st: 200 fsqrt ; st: sqrt(200) fld st0 ; do st(0) wczytaj st(0). ; st: sqrt(200), sqrt(200) frndint ; zaokrąglij do liczby całkowitej. ; st: (int)sqrt(200), sqrt(200) fcompp ; porównaj i zdejmij oba. fstsw ax sahf mov ah,9 je tak200 mov dx,nie_200 jmp short pisz_200 tak200: mov dx,tak_200 pisz_200: int 21h ; wypisz stosowną wiadomość mov ax,4c00h int 21h dwa_pie_sze dw 256 dwiescie dw 200 szesnascie dw 16 nie_256 db "SQRT(256) != 16$" tak_256 db "SQRT(256) = 16$" nie_200 db "Liczba 200 nie jest kwadratem liczby calkowitej$" tak_200 db "Liczba 200 jest kwadratem liczby calkowitej$"
Dwa ostatnie programiki zbiłem w jeden i przetestowałem. Wyszło, że sinus PI jest różny od zera, reszta była prawidłowa.
Oczywiście, w tych przykładach nie użyłem wszystkich instrukcji koprocesora (nawet spośród tych, które wymieniłem). Mam jednak nadzieję, że te proste programy rozjaśnią nieco sposób posługiwania się koprocesorem.
Ładnie brzmi, prawda? Ale co to takiego?
Otóż, bardzo dawno temu pewien polski matematyk, Jan Łukasiewicz, wymyślił taki sposób zapisywania działań, że nie trzeba w nim używać nawiasów. Była to notacja polska. Sposób ten został potem dopracowany przez Charlesa Hamblina na potrzeby informatyki - w ten sposób powstała Odwrotna Notacja Polska.
W zapisie tym argumenty działania zapisuje przed symbolem tego działania. Dla jasności podam teraz kilka przykładów:
Zapis tradycyjny ONP a+b a b + a+b+c a b + c + ; ab+ stanowi pierwszy argument ; drugiego dodawania c+b+a c b + a + (a+b)*c a b + c * c*(a+b) c a b + * (a+b)*c+d a b + c * d + (a+b)*c+d*a a b + c * d a * + (a+b)*c+d*(a+c) a b + c * d a c + * + (a+b)*c+(a+c)*d a b + c * a c + d * + (2+5)/7+3/5 2 5 + 7 / 3 5 / +
Ale po co to komu i dlaczego mówię o tym akurat w tej części?
Powód jest prosty: jak się dobrze przyjrzeć zapisowi działania w ONP, to można zobaczyć, że mówi
on o kolejności działań, jakie należy wykonać na koprocesorze. Omówimy to na przykładzie:
Zapis tradycyjny (jeden z powyższych przykładów): (a+b)*c+(a+c)*d Zapis w ONP: a b + c * a c + d * + Uproszczony kod programu: fld [a] fld [b] faddp ; NASM: faddp st1, st0 fld [c] fmulp ; NASM: fmulp st1, st0 fld [a] fld [c] faddp ; NASM: faddp st1, st0 fld [d] fmulp ; NASM: fmulp st1, st0 faddp ; NASM: faddp st1, st0 Teraz st0 jest równe wartości całego wyrażenia.
Jak widać, ONP znacznie upraszcza przetłumaczenie wyrażenia na kod programu. Jednak, kod nie jest optymalny. Można byłoby na przykład zachować wartości zmiennych a i c na stosie i wtedy nie musielibyśmy ciągle pobierać ich z pamięci. Dlatego w krytycznych sekcjach kodu stosowanie zasad ONP nie jest zalecane. Ale w większości przypadków Odwrotna Notacja Polska sprawuje się dobrze i uwalnia programistów od obowiązku zgadywania kiedy i jakie działanie wykonać.
Pamiętajcie tylko, że stos koprocesora może pomieścić tylko 8 liczb!
Następnym razem o SIMD.