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ă:

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:

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ă și 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.
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 <LABEL>

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 argument o valoare întreagă de 2/4 octeți (word/dword).

Lista completă a instucțiunilor poate fi văzută 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

Feedback

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 să completați formularul de feedback de pe site-ul acs.curs.pub.ro, la cursul specific seriei din care faceți parte. Trebuie să fiți autentificați și înrolați în cadrul cursului.

Formularul este anonim și este activ în perioada 6-17 ianuarie 2020. Rezultatele vor fi vizibile în cadrul echipei cursului doar după încheierea sesiunii.

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!

Formularele de feedback sunt următoarele:

Exerciții

Pentru exercițiile din acest laborator vom folosi fișierele din această arhivă. Rezolvarea exercițiilor o vom face în SASM.

[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.

[1.5p] 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.

[1.5p] 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 <adresa>, 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 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ă).

iocla/laboratoare/laborator-12.txt · Last modified: 2020/01/16 21:59 by bogdan.purcareata
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0