====== Laborator 12: Calcul în virgulă mobilă ======
Până acum, pe parcursul laboratorului de IOCLA am învățat despre cum putem efectua operații cu numere întregi, pozitive sau negative, de diverse dimensiuni. Dar cum rămâne cu numerele cu virgulă? Multe din procesoarele folosite astăzi dispun de o unitate de calcul în virgulă mobilă (**Floating Point Unit** - FPU), care oferă funcții de calcul pentru astfel de valori.
În acest laborator vom vedea cum putem folosi instrucțiunile puse la dispoziție de FPU pentru a efectua eficient calcule în virgulă mobilă.
===== Reprezentarea numerelor în virgulă mobilă ====
Primul aspect pe care trebuie să înțelegem este cum putem să reprezentăm numerele cu virgulă în formă binară. Ca și în cazul numerelor întregi, valorile fracționare se reprezintă pe un anumit număr de biți.
Principalele obiective ale unei reprezentări pe un anumit număr de biți sunt:
* posibilitatea de a reprezenta cât mai multe valori intre valoarea minimă și valoarea maximă
* o precizie cât mai bună a valorilor (numărul maxim de cifre după virgulă)
Pentru a reprezenta valorile fracționare vom folosi **Reprezentarea în virgulă mobilă** (**Floating Point Representation**). În această reprezentare, numerele au următoarea structură:
{{ :laboratoare:lab12-equation1.png?500 |}}
După cum putem observa mai sus, valorile trebuie transformate astfel încât partea întreagă să fie 1. Această formă poartă
numele de formă normală, iar operația de transformare în această formă poartă numele de **normalizare**.
În forma binară, valorile se reprezintă astfel:
{{ :laboratoare:2000px-ieee_754_double_floating_point_format.png?600 |}}
**Semnul** este dat de primul bit din reprezentarea binară: 1 = negativ, 0 = pozitiv.\\
**Mantisa** dă partea fracționară a numărului în forma normală. Numărul de biți pe care mantisa este reprezentată dă precizia maximă a reprezentării.\\
**Baza** este de obicei 2, 10 sau 16 și este dată de standardul de reprezentare ales.\\
**Exponentul** dă valoarea la care este ridicată baza și numărul de biți pe care este reprezentat dă valorile valorile maxime și minime ce pot fi reprezentate.\\
==== Standarde de reprezentare în virgulă mobilă ====
Cele mai folosite standarde de reprezentare în virgulă mobilă sunt cu precizie simplă (**Single Precision**) și cu precizie dublă (**Double Precision**).
Reprezentarea cu **precizie simplă** presupune folosirea a 32 de biți și corespunde valorilor float din limbajul C. În acest caz baza folosită este 2, exponentul are 8 biți, iar restul de 23 de biți corespund mantisei.
Reprezentarea cu **precizie dublă** presupune folosirea a 64 de biți și corespunde valorilor de tip double. În acest caz, baza este 2, exponentul are 11 biți, iar restul de 52 de biți corespund mantisei.
În ambele cazuri, exponentul folosit în calcul nu este cel extras din numărul în forma binară, ci este calculat astfel:
* În precizie simplă, exponentul este dat de valoarea ''(exponentul pe 8 de biți) - 127'' (adică se scade o valoare fixă din valoarea efectivă a exponentului)
* În precizie dublă, exponentul este dat de valoarea ''(exponentul pe 11 de biți) - 1023'' (adică se scade o valoare fixă din valoarea efectivă a exponentului)
==== Valori speciale ====
În ambele standarde de reprezentare, valorile ce au ca exponent 0 sau valoarea maximă (255 pentru precizie simplă șu 2047 pentru precizie dublă) sunt valori speciale. Printre acestea se numără:
* Valoarea 0: toți biții (de semn, exponent și mantisă) sunt 0
* Valorile +/-infinit, ce rezultă în urma oricărei împărțiri la 0. Aceste valori se reprezintă prin mantisă 0 și exponent maxim (255 pentru precizie simplă și 2047 pentru precizie dublă).
* Valoarea NaN (not a number), ce rezultă în urma operațiilor invalide, precum 0/0 sau infinit - infinit. Această valoarea este reprezentată prin exponent maxim și mantisă diferită de 0.
===== Unitatea de calcul în virgulă mobilă ====
În procesoarele vechi, calculele matematice erau efectuate pe un chip separat, numit **coprocesor matematic** care comunica cu procesorul principal. Procesoarele moderne au acest chip încorporat sub forma Floating Point Unit, care le permite să efectueze eficient calcule matematice cu numere fracționare folosind un set separat de instrucțiuni.
Unitatea de calcul în virgulă mobilă deține o stivă proprie pe care o folosește pentru a citi operanzii și a stoca rezultatele operațiilor.
De exemplu, o instrucțiune de tip adunare scoate primele două valori de pe această stivă, efectuează operația de adunare și pune rezultatul înapoi în vârful stivei. Deci, înainte de efectuarea oricărei operații va trebui să punem operanzii necesari pe stivă și, după efectuarea operației, vom obține rezultatul folosind o instrucțiune de tip pop.
===== Instrucțiuni pentru calcul în virgulă mobilă =====
==== Instrucțiuni de tip push ====
^ Instrucțiune ^ Descriere ^
| fld1 | Stochează constanta 1 în vârful stivei |
| fldz | Stochează constanta 0 în vârful stivei |
| fldpi | Stochează constanta π în vârful stivei |
| fld DWORD [registru] | Stochează valoarea de tip float (4 octeți) de la adresa indicată de registru |
| fild DWORD [registru] | Stochează valoarea de tip integer (4 octeți) de la adresa indicată de registru |
| fld QWORD [registru] | Stochează valoarea de tip double (8 octeți) de la adresa indicată de registru |
| fld st0 | Duplică valoarea din vârful stivei |
| fxch | Interschimbă primele două valori de pe stivă . |
==== Instrucțiuni de tip pop ====
^ Instrucțiune ^ Descriere ^
| fstp DWORD [registru] | Citește o valoare de tip float (4 octeți) din vârful stivei și o salvează la adresa indicată de registru. Valoarea este eliminată de pe stivă. |
| fst DWORD [registru] | Similar cu instrucțiunea anterioară, dar valoarea rămâne în vârful stivei. |
| fstp QWORD [registru] | Citește o valoare de tip double (8 octeți) din vârful stivei și o salvează la adresa indicată de registru, eliminând valoarea de pe stivă |
| fst QWORD [registru] | Similar cu instrucțiunea anterioară, dar valoare rămâne în vârful stivei. |
==== Instrucțiuni de comparație ====
^ Instrucțiune ^ Descriere ^
| fcom | Compară primele două valori de pe stivă și setează flag-urile interne FPU. |
| fcomip | Similar cu prima instrucțiune, dar elimină prima valoare din vârful stivei |
| fcomi | Similar cu prima instrucțiune, dar setează flag-urile ZF, PF și CF din registrul EFLAGS. |
| fcomip | Similar cu prima instrucțiune, dar setează flag-urile și elimină prima valoare din vârful stivei |
| ficom word [registru] | Compara prima valoare de pe stivă cu un număr întreg pe 2 octeți de la adresa indicată de registru |
| ficom dword [registru] | Compară prima valoare de pe stivă cu un număr întreg pe 4 octeți de la adresa indicată de registru |
| ficom qword [registru] | Compară prima valoare de pe stivă cu un număr întreg pe 8 octeți de la adresa indicată de registru |
| ficomp word/dword/qword [registru] | Similar cu instrucțiunile anterioare, dar elimină și prima valoare de pe stivă |
| ftst | Compară prima valoare de pe stivă cu 0.0. |
Doar instrucțiunile fcomi și fcomip setează flag-urile din registrul EFLAGS. Restul instrucțiunilor modifică doar un registru de flag-uri intern FPU-ului. Cu alte cuvinte, dacă folosiți o altă instrucțiune nu puteți face un salt condiționat de rezultatul instrucțiunii, pentru că flag-urile sunt nealterate. Pentru a putea face acest lucru trebuie să citiți registrul intern de flag-uri din FPU și să setați flag-urile din EFLAGS conform cu acesta. Puteți face acest lucru ca în exemplul următor:
ftst ; compară valoarea din vârful stivei cu +0.0
fstsw ax ; copiază registrul de stare intern FPU-ului în AX
fwait ; așteaptă completarea instrucțiunii precedente
sahf ; copiază flag-urile din AX în registrul de stare EFLAGS
; acum flag-urile sunt setate în funcție de rezultatul instrucțiunii FTST
jg
==== Instrucțiuni matematice ====
^ Instrucțiune ^ Descriere ^
| fabs | Înlocuiește prima valoare de pe stivă cu valoarea ei absolută. |
| fchs | Change sign - schimbă semnul valorii din vârful stivei. |
| frndint | Round to integer - rotunjește prima valoare de pe stivă la întreg. |
| fadd dword/qword [registru] | Adună prima valoare de pe stivă cu cea indicată de adresa din registru (pe 4/8 octeți). |
| fdiv dword/qword [registru] | Împarte prima valoare de pe stivă cu cea indicată de adresa din registru (pe 4/8 octeți) |
| fdivr dword/qword [registru] | Similar cu instrucțiunea precedentă, dar împărțirea este inversă. |
| fmul dword/qword [registru] | Înmulțește prima valoare de pe stivă cu cea indicată de adresa din registru (pe 4/8 octeți). |
| fsub dword/qword [registru] | Scade din prima valoare de pe stivă valoarea indicată de adresa din registru (pe 4/8 octeți). |
| fsubr dword/qword [registru] | Similar cu instrucțiunea anterioară, dar ordinea operanzilor se schimbă |
| fsqrt | Înlocuiește prima valoare de pe stivă cu rădăcina ei pătrată |
| fsin | Înlocuiește prima valoare de pe stivă cu rezultatul funcției sin. |
| fcos | Înlocuiește prima valoare de pe stivă cu rezultatul funcției cos. |
Instrucțiunile ''fiadd'', ''fisub'', ''fidiv'', ''fimul'' funcționează exact ca cele din tabelul de mai sus, dar primesc ca arguument o valoare întreagă de 2/4 octeți (word/dword).
Lista completă a instucțiunilor poate fi văzută [[https://docs.oracle.com/cd/E18752_01/html/817-5477/eoizy.html|aici]]
==== Exemple ====
Mai jos este prezentat un exemplu de adunare a două numere cu virgulă, reprezentate pe 8 octeți (de tip double). Am folosit valori double pentru a putea folosi printf pentru afișare, întrucât acesta nu poate afișa valori cu precizie simplă.
%include "io.inc"
extern printf
section .data
n1 dq 1.1
n2 dq 4.3
format db "%f", 10, 0
section .text
global CMAIN
CMAIN:
mov ebp, esp
fld qword [n1]
fadd qword [n2]
sub esp, 8 ; rezervă loc pe stivă pentru rezultat, pasat ca argument funcției printf.
fstp qword [esp]; mută rezultatul adunării în spațiul rezervat.
push format
call printf
add esp, 12
ret
Exemplul de mai jos citește o valoare de tip double de la tastatură și afișează rezultatul expresiei sin(x * pi).
%include "io.inc"
extern printf
extern scanf
section .data
x dq 0.0
scan_format db "%lf"
print_format db "sin(pi * x) = %f", 10, 0
section .text
global CMAIN
CMAIN:
mov ebp, esp
push x
push scan_format
call scanf
add esp, 8
fldpi
fmul qword [x]
fsin
sub esp, 8
fstp qword [esp]
push print_format
call printf
add esp, 12
ret
===== Exerciții ======
Pentru exercițiile din acest laborator vom folosi fișierele din [[http://elf.cs.pub.ro/asm/res/laboratoare/lab-12-tasks.zip|această arhivă]]. Rezolvarea exercițiilor o vom face în SASM.
==== [1p] 0. Completare formular de feedback IOCLA ====
Pentru a îmbunătăți cursul de IOCLA, componentele sale și modul de desfășurare, ne sunt foarte utile opiniile voastre. Pentru aceasta, vă rugăm, să accesați și completați [[http://cs.curs.pub.ro/2015/blocks/simplehtml/view.php?courseid=276&blockid=1468
|formularul de feedback de pe site-ul cs.curs.pub.ro]]. Trebuie să fiți autentificați și înrolați în cadrul cursului.
Formularul este anonim și este activ în perioada 4-20 ianuarie 2016. Rezultatele vor fi vizibile în cadrul echipei cursului doar după încheierea sesiunii. Găsiți [[http://cs.curs.pub.ro/2015/blocks/simplehtml/view.php?courseid=276&blockid=1468
|formularul]] în partea dreaptă a paginii principale de IOCLA de pe cs.curs.pub.ro într-un frame intitulat %%"FEEDBACK"%%,
Vă invităm să evaluați activitatea echipei de IOCLA și să precizați punctele tari și punctele slabe și sugestiile voastre de îmbunătățire a disciplinei. Feedback-ul vostru este foarte important pentru noi să creștem calitatea materiei în anii următori și să îmbunătățim disciplinele pe care le veți face în continuare.
Ne interesează în special:
* Ce nu v-a plăcut și ce credeți că nu a mers bine?
* De ce nu v-a plăcut și de ce credeți că nu a mers bine?
* Ce ar trebuie să facem ca lucrurile să fie plăcute și să meargă bine?
Vă mulțumim!
==== [1p] 1. Tutorial: Suma unui vector de numere fracționare ====
În fișierul ''suma.asm'' din subdirectorul ''ex1/'' găsiți un exemplu de adunare a valorilor dintr-un vector de numere fracționare. La final, rezultatul este afișat folosind funcția ''printf''.
Parcurgeți și înțelegeți exemplul dat. Deschideți-l folosind SASM și rulați-l.
==== [1p] 2. Media unui vector de numere fracționare ====
Acum că am văzut cum putem calcula suma elementelor unui vector, ne propunem să calculăm media acestora. Pentru aceasta trebuie să calculăm mai întai suma (vă puteți inspira din exemplul anterior) și să împărțim la numărul de elemente.
Urmăriți comentariile marcate cu ''TODO'' din fișierul ''media.asm'' din subdirectorul ''ex2/'' și completați corespunzător pentru calcularea mediei vectorului de elemente ''vector''.
Împărțirea se face la un număr întreg (adică se împarte la lungimea vectorului ''vector''). Folosiți instrucțiunea ''fidiv'' pentru a împărți valoarea din vârful stivei FPU la un număr cu reprezentare de număr întreg.
==== [1p] 3. Împărțirea a două numere întregi cu rezultat fracționar ====
În fișierul ''integer-div.asm'' din subdirectorul ''ex3/'', completați locurile marcate cu ''TODO'' pentru a efectua împărțirea numerelor întregi ''n1'' și ''n2'', iar rezultatul să fie un număr fracționar. La final, afișați rezultatul.
Pentru a împărți la o valoare întreagă (la un număr cu rezentare de număr întreg) folosiți instrucțiunea ''fidiv''.
Deîmpărțitul trebuie să fie încărcat în prealabil (**înaintea** împărțirii folosind ''fidiv'') pe stiva FPU tot ca întreg. Pentru a încărca un număr cu reprezentare ca număr întreg pe stiva FPU folosiți instrucțiunea ''fild''.
==== [2p] 4. Extragere parte întreagă și fracționară =====
Completați secțiunile marcate cu ''TODO'' din fișierul ''extract.asm'' din subdirectorul ''ex4/'', pentru a extrage partea întreagă și partea fracționară a numărului fracționar ''n''.
Pentru a extrage partea întreagă puteți folosi instrucțiunea ''fisttp dword '', care extrage prima valoare din vârful stivei FPU, o trunchiază și o stochează ca un întreg la adresa specificată.
Instrucțiunea ''fisttp'' este o instrucțiune validă, chiar dacă nu este colorată în ''SASM''.
Instrucțiunea ''fisttp'' face și pop la valoarea din vârful stivei FPU. De aceea, pentru obținerea părții fracționare, va trebui să reîncărcați valoarea numărului fracționar ''n'' (folosind ''fld'') înainte de scădea valoarea întregii din acesta folosind instrucțiunea ''fisub''.
==== [2p] 5. Media unui vector de întregi cu rezultat fracționar ====
Similar cu exercițiul 2, ne propunem să calculăm media unui vector de elemente, dar în acest caz valorile sunt întregi. Rezultatul trebuie, bineînțeles, să fie fracționar. Completați locurile marcate cu ''TODO'' din fișierul ''media-int.asm'', directorul ''ex5''.
Întrucât numerele care se adună sunt întregi veți folosi pentru aceasta instrucțiunea ''fiadd'' care adună numere în reprezentare de număr întreg.
De asemenea, când faceți împărțirea, veți folosi instrucțiunea ''fidiv''.
Rezultatul operației de împărțire va fi unul fracționar, deci îl veți recupera folosind instrucțiunea ''fstp''.
==== [2p] 6. Maximul dintr-un vector de numere fracționare ====
Completați fișierul ''max.asm'' din subdirectorul ''ex6/'' pentru a afla valoarea maximă din vector. Parcurgerea elementelor și comparația cu valoarea maximă este implementată. Urmăriți comentariile ce conțin ''TODO''.
Pentru a determina instrucțiunea de jump folosită după ''fcomip'' urmăriți răspunsul de [[http://stackoverflow.com/a/7057771|aici]].
Pentru a scoate prima valoare de pe stiva FPU fără a o stoca la o adresă, puteți folosi instrucțiunea ''fstp ST0''.
Valoarea variabilei ''max'' trebuie să ajungă pe stivă. Întrucât valoarea are 8 octeți sunt necesare două operații de tip ''%%push dword ...%%''. Întâi faceți push la ultimii 4 octeți ai valorii variabilei ''max'' și apoi la primii 4 octeți. Astfel vârful stivei va referi primii 4 octeți ai valorii variabilei ''max'' urmați de ultimii 4 octeți.
==== [2p Bonus] 7. Implementare arcsin ====
În fișierul ''arcsin.asm'' din subdirectorul ''ex7/'', calculați valoarea unghiului pentru care funcția sin întoarce valoarea din variabila ''valoare_sin''.
Pentru rezolvare veți căuta în intervalul [0, pi/2], folosind metoda bisecției: la fiecare pas calculați valoarea funcției sin în valoarea de la jumătatea intervalului și alegeți jumătatea de interval în care se găsește valoarea căutată (funcția este crescătoare pe intervalul [0, pi/2]). Algoritmul se va opri atunci când eroarea este mai mică decât ''0.0005'' (diferența între valoarea obținută și valoarea căutată).