1 Organizzazione dell’unità in virgola mobile (FPU)
Il processore può maneggiare anche numeri reali, attraverso un modulo detto Floating-Point Unit
(FPU). Storicamente, tale circuiteria è stata inizialmente realizzata su un chip a sé stante, detto coprocessore matematico (o numerico), che poteva essere presente o meno in un sistema. A partire dai
processori 486 dell’architettura Intel x86 è stato integrato dentro un unico chip insieme alla ALU.
La FPU ha un proprio set di registri ed un set di istruzioni che la riguardano, essenzialmente riservati a fare i conti con i numeri reali. La sua organizzazione è molto diversa rispetto a quella della ALU,
ed è specializzata per gli scopi (più limitati) ai quali la FPU serve.
1.1 Aritmetica dei numeri reali
Dato un numero in base β , la sequenza di cifre ( an −1 , an − 2 ,..., a1 , a0 .a−1 , a−2 ,..., a− m ) intervallate da un
punto chiamato separatore decimale corrisponde al numero razionale
A=
n −1
n −1
m
∑ a ⋅β = ∑a ⋅β +∑a
i
i =− m
i
i
i =0
i
i =1
−i
⋅ β −i
In particolare, in base 10:
23.85 = 2 ⋅101 + 3 ⋅100 + 8 ⋅10−1 + 5 ⋅10 −2
Ed in base 2:
11.01 = 1 ⋅ 21 + 1 ⋅ 20 + 0 ⋅ 2 −1 + 1 ⋅ 2 −2 = 3.25
Alcuni numeri razionali e/o reali richiedono un numero infinito di cifre per essere rappresentati in
una data base, ad esempio le rappresentazioni del numero razionale 1 3 e del numero reale
3 ri-
chiedono un numero infinito di cifre in base 10. Usando un numero finito di cifre si può rappresentare soltanto un sottoinsieme dei numeri razionali. Benché nel calcolatore si utilizzi un numero finito di cifre, per uniformità di lessico con tutti i testi sull’argomento, parleremo di rappresentazione
dei numeri reali all’interno del calcolatore.
La conversione di un numero con parte frazionaria non nulla da una base ad un’altra si fa convertendo separatamente la parte intera e quella frazionaria. La parte intera viene convertita come abbiamo già visto. Quella frazionaria viene convertita come segue:
-
da base 2 a base 10: in modo ovvio, moltiplicando ciascuna cifra per la potenza (negativa) di
due che le compete, come nell’esempio
-
da base 10 a base 2: usando un algoritmo duale di quello delle divisioni successive:
Supponiamo di disporre di due funzioni int ( ⋅) , frac ( ⋅) , che calcolano la parte intera e frazionaria di
un numero. Possiamo quindi scrivere l’algoritmo come segue
F1 = frac ( A)
I i = int ( 2 ⋅ Fi ) , Fi +1 = frac ( 2 ⋅ Fi )
E la sequenza delle cifre binarie I i , i ≥ 1 , costituisce il risultato che cerco, cioè I i = a− i . Vediamo
un esempio:
0.85 ⋅ 2 = 1.7
Moltiplichiamo ogni volta per due la parte frazionaria. La parte intera otte-
0.7 ⋅ 2 = 1.4
nuta (che vale 0 o 1) rappresenta la prossima cifra in base due della parte
0.4 ⋅ 2 = 0.8
frazionaria. La nuova parte frazionaria viene usata per la successiva molti-
0.8 ⋅ 2 = 1.6
0.6 ⋅ 2 = 1.2
0.2 ⋅ 2 = 0.4
plicazione. L’algoritmo termina quando si ottiene una parte frazionaria nulla,
o quando la precisione richiesta viene raggiunta.
Dall’esempio si nota che ci sono numeri che sebbene in base 10 si rappresentino con un numero finito di cifre decimali, in base 2 richiedono un numero infinito di cifre decimali. Infatti, nel caso soprastante, si ottiene una parte frazionaria periodica di periodo 4. Quindi, la rappresentazione in base
2 dovrebbe essere 10111.110110 . In particolare, visto che 2 è un sottomultiplo di 10, tutti i numeri
che hanno una parte frazionaria finita in base 2 avranno una parte frazionaria finita in base 10, ma
non viceversa.
Dato che i numeri reali sono rappresentati in base 2 nel calcolatore, si avranno spesso delle appros-
simazioni, che devono essere tenute in conto dal programmatore.
1.1.1 Standard di rappresentazione IEEE 754
Guardiamo come prima cosa come sono rappresentati i numeri reali nel PC. Lo standard si chiama
IEEE 754, e descrive due tipi di numeri: precisione singola (32 bit, float), precisione doppia (64 bit,
double). Lo stile di rappresentazione è identico nei due casi, cambia solo il numero di bit dei campi.
Un numero in virgola mobile, secondo lo standard IEEE è rappresentato su 32 o 64 bit, divisi in tre
parti:
-
un bit di segno s;
-
un numero intero e, detto esponente;
-
un numero frazionario M, detto mantissa,
nell’ordine sopra riportato. I bit di una parola di n bit sono indicizzati in modo decrescente con numeri interi da 0 a n-1. In un numero in questo standard, l'importanza del bit decresce col suo indice.
Di seguito è rappresentato un numero reale su 32 bit:
1
8
23
+-+--------+-----------------------+
|S| E
| Mantissa
|
+-+--------+-----------------------+
31 30
22
0
lunghezza in bit
indice dei bit
Il valore del numero reale rappresentato è: ( −1) ⋅ 2e ⋅ M
s
Il campo s specifica il segno del numero: 0 per i numeri positivi, 1 per i numeri negativi.
La mantissa è un numero frazionario normalizzato, la cui parte intera è cioè pari a 1. Pertanto, la
parte intera non viene rappresentata. L’esponente viene aggiustato di conseguenza in modo da riportare il numero ad una mantissa con parte intera unitaria.
Il campo E contiene la rappresentazione dell’esponente. Essendo costituito da 8 bit, permette di
rappresentare 256 valori. Due combinazioni di bit della rappresentazione E (0 e 255) sono riservate
per funzioni speciali (descritte in seguito); sono quindi possibili 254 combinazioni per rappresentare
gli esponenti. L’esponente è un numero intero: ciononostante non è rappresentato in complemento a
2. Infatti, quando si ha a che fare con numeri reali, fa comodo che la rappresentazione degli esponenti sia monotona (in modo da poter vedere facilmente se un numero è più grande o più piccolo di
un altro). Pertanto l’esponente è rappresentato in traslazione. La sua rappresentazione è il numero
naturale E = e + pol , con pol = 127 . In tal modo, gli esponenti da -126 a +127 hanno come rappresentazione le stringhe di bit corrispondenti ai naturali da 1 a 254.
L’intervallo
di
rappresentabilità
con
questo
standard
di
rappresentazione
va
da
±1.000...000 ⋅ 2−126 ≈ 1.17 ⋅10−38 a ±1.111...111 ⋅ 2127 ≈ 3.4 ⋅1038 . Per ogni potenza di due abbiamo la
stessa quantità di numeri (223), tutti con lo stesso numero di cifre significative (23). Il fattore di polarizzazione dell’esponente è stato scelto in modo tale che il numero più piccolo possibile abbia un
inverso entro l’intervallo di rappresentabilità. La rappresentazione in virgola mobile consente di avere sempre la stessa precisione relativa, sia che si rappresentino numeri grandi (esponente positivo
elevato), che numeri piccoli (esponente negativo grande in modulo).
Esempio:
Rappresentiamo il numero −118.625 su 32 bit nello standard IEEE 754. Dobbiamo determinarne il
segno, l'esponente e la mantissa. Poiché è un numero negativo, il segno (primo bit) è "1". Poi scriviamo il valore assoluto del numero in forma binaria, convertendolo con gli algoritmi già visti:
1110110.101.
Successivamente normalizziamo il numero: spostiamo la virgola verso sinistra, lasciando solo un 1
alla sua sinistra:
1110110.101 = 1.110110101·26
La mantissa è la parte a destra della virgola, completata con zeri a destra fino a riempire i 23 bit:
11011010100000000000000.
L'esponente è pari a 6, ma va convertito in binario e traslato di 127. Quindi 6 + 127 = 133. In forma
binaria: 10000101. Assemblando il tutto:
1
8
23
+-+--------+-----------------------+
|S|
E
| Fraction
|
|1|10000101|11011010100000000000000|
+-+--------+-----------------------+
31 30
22
0
Il numero zero non può essere rappresentato in modo normalizzato: ciò richiederebbe infatti un esponente pari a −∞ . Per convenzione, viene rappresentato con esponente e mantissa nulli. Esistono
quindi due “zeri”, uno con segno positivo, uno con segno negativo.
Osserviamo che il numero più piccolo (in modulo) rappresentabile in modo normalizzato è:
±1.000...000 ⋅ 2 −126 ≈ 10−38
Che succede quando un’operazione produce un risultato x più piccolo (in modulo) del minimo numero rappresentabile in forma normalizzata? Si ha un underflow, che è una condizione anomala.
Dovrei approssimare x con il più vicino numero rappresentabile, che è lo zero. Fare questo comporterebbe una perdita di precisione notevole (infatti, sparirebbero d’un colpo solo tutte le cifre significative della mantissa di x ), e potrebbe creare problemi nei conti successivi. Ad esempio, una
successiva moltiplicazione x ⋅ y , con y ≫ 1 , darebbe in questo caso un risultato nullo, anche se in
teoria perfettamente rappresentabile.
Per gestire questa situazione limitando i danni, la FPU gestisce anche numeri denormalizzati. Un
numero denormalizzato è un numero la cui mantissa ha una parte intera nulla.
Vediamo con un esempio come si rappresentano i numeri denormalizzati:
Supponiamo che il risultato di un’operazione, rappresentato in modo normalizzato, sia:
s = 1, e = −129, m = 1.010111000...
Ovviamente, l’esponente -129 non può essere rappresentato sul numero di bit assegnato
all’esponente nel formato dei reali a 32 bit. Per poter rappresentare questo numero, dovrei scalare la
mantissa all’indietro di tre posizioni, rappresentando quindi:
s = 1, e = −126, m = 0.001010111000...
Il problema è che adesso la rappresentazione non sarebbe corretta. Infatti, visto che non rappresento
il bit intero della mantissa, non sarei in grado di distinguerla da quella del numero:
s = 1, e = −126, m = 1.001010111000...
Per evitare ambiguità, i numeri denormalizzati sono rappresentati usando il valore minimo per
l’esponente E polarizzato (cioè zero). La convenzione è che quando la rappresentazione
dell’esponente è zero, allora il numero si intende come denormalizzato, e quindi la sua mantissa ha
parte intera nulla.
Come già visto, per rappresentare un numero denormalizzato le cifre della mantissa vengono fatte
scorrere verso destra. Dato che la mantissa ha un numero finito di cifre, questo comporta la perdita
di cifre significative, tanto maggiore quanto più il numero da rappresentare si avvicina allo zero. Ad
ogni buon conto, perdere qualche cifra significativa gradualmente comporta meno problemi che
perderle tutte insieme.
Si noti che, nel caso di numero denormalizzato, l’esponente di due per cui la mantissa si intende
scalata è comunque -126, e non -127 (anche se la rappresentazione in traslazione dell’esponente
corrisponderebbe al numero -127).
La rappresentazione con i bit di E pari a zero è un caso particolare. Ce ne sono altri, elencati nella
tabella seguente:
Categoria
E
Mantissa
Zeri
0
0
Numeri denormalizzati
0
non zero
Numeri normalizzati
1-254
qualunque
Infiniti
255 (massimo) 0
Quiet NaN (Not a Number) 255 (massimo) non zero, MSB=1
Signaling NaN
255
non zero, MSB=0
Una configurazione con tutti i bit a 0 (mantissa e rappresentazione dell’esponente) indica il numero
zero. Ne esistono due, a seconda del segno. È infatti comodo poter rappresentare in modo semplice
gli intervalli che comprendono lo zero. Inoltre, quando un’operazione dà risultato zero, il segno può
rivelare alcune informazioni importanti, ad esempio se si è arrivati a zero come limite sinistro o destro.
Esistono anche rappresentazioni di infinito (positivo e negativo), e di risultati indefiniti (NaN). Un
risultato infinito si ha quando, ad esempio, si divide un numero molto grande per uno molto piccolo
in modulo. Un risultato indefinito si ha, ad esempio, effettuando operazioni come +∞ − ∞ , oppure
0 0, o
−1 . I NaN “quiet” non generano eccezioni, mentre i NaN “signaling” generano eccezioni
se si trovano come operandi in una istruzione.
La rappresentazione dei reali a doppia precisione è molto simile alla precedente, ma usa un numero
di bit maggiore per codificare i campi:
1
11
52
+-+-----------+----------------------------------------------------+
|S|
E
| mantissa
|
+-+-----------+----------------------------------------------------+
63 62
51
0
Valgono gli stessi ragionamenti fatti per quella singola (si veda la tabella), considerando che
l’esponente è rappresentato su più bit. L’intervallo di rappresentazione va da 10 −308 a 10308 circa.
L’esponente ha un fattore di polarizzazione pari a 1023 (quindi e = E − 1023). Il valore massimo di
E è pari a 2047, e tale valore è riservato ai NaN e agli infiniti.
Nella FPU dell’architettura x86 si possono rappresentare numeri a precisione estesa (80 bit).
1
15
64
+-+-----------+----------------------------------------------------+
|S|
E
| mantissa
|
+-+-----------+----------------------------------------------------+
79 78
64
0
Nei reali a precisione estesa, la mantissa contiene anche il bit intero (che negli altri formati è invece
omesso). Per il resto, valgono le stesse proprietà viste per gli altri due formati (si veda anche la tabella per i casi particolari), considerando che l’esponente è rappresentato su più bit. L’intervallo di
rappresentazione va da 10 −4930 a 10 4930 circa. L’esponente ha un fattore di polarizzazione pari a
16383 (quindi e = E − 16383 ). Il valore massimo di E è pari a 32767, e tale valore è riservato ai
NaN e agli infiniti. L’unica differenza sostanziale rispetto alle altre due rappresentazioni sta nel fatto che nella mantissa viene rappresentata anche la parte intera (un bit, pari a 1 per i numeri normalizzati e zero per i numeri de normalizzati).
1.2 Descrizione della FPU
La FPU è dotata di 8 registri a 80 bit (atti, quindi, a contenere numeri reali in precisione estesa), organizzati a stack (pila FPU), più un registro di stato (che contiene anche flag specifici alla FPU) ed
un registro di controllo¸ che contiene informazioni di configurazione per l’unità. La FPU (come del
resto la ALU) ha anche altri registri, invisibili al programmatore, che non verranno descritti.
I registri della pila FPU contengono gli operandi e i risultati. Tutti gli operandi reali che vengono
portati nella pila vengono automaticamente estesi a 80 bit, qualunque sia la loro lunghezza (a meno
di configurare la FPU in modi particolari, cosa che non faremo). In maniera duale, quando un risultato reale viene scritto in qualche locazione di memoria (di 32, 64, o 80 bit) viene approssimato nel
formato corretto. Dentro la FPU, tutti gli operandi sono su 80 bit. Il motivo è che conviene fare i
conti a precisione maggiore.
I registri sono otto. La pila FPU si estende da R7 fino a R0. Il registro che si trova, ad un dato istante, ad essere in cima alla pila FPU, viene riferito nel codice come ST o ST(0). Allo stesso modo, il
registro immediatamente sottostante (il penultimo riempito) sarà ST(1), e via discorrendo. I registri
possono essere indirizzati in modo relativo al top della pila usando questa sintassi.
La pila FPU viene riempito da R7 a R0, ed è considerata circolare (R0 è adiacente a R7). Se si immette un nuovo valore quando R0 è già pieno, si ha wraparound (il valore in R7 viene sovrascritto),
e questa cosa può dar luogo ad un’eccezione e può essere rilevata dal programmatore controllando
opportuni bit nel registro di stato.
Figura 1 – schema logico della FPU
Figura 2 – registro di stato della FPU
Il registro di stato (Status Register) è a 16 bit. Il bit 15 contiene un valore non significativo. Il resto
del registro contiene tre tipi di informazione:
-
bit 11-13: indicazione del top dello stack. Contiene (su 3 bit) l’indice dell’ultimo registro riempito nella pila FPU. Inizialmente vale 000, ad indicare che il primo registro riempito sarà R7.
bit 7-0: flag delle eccezioni. Quando qualcuno di questi bit è a 1, si è verificata una condizione anomala durante l’esecuzione di un’istruzione. A seconda di come è impostato il registro di controllo,
ciò può richiedere l’esecuzione una routine di eccezione, che gestisce la condizione anomala. Il bit 7
(exception flag) riassume se si è verificata una condizione che ha richiesto l’esecuzione di una routine di eccezione. Il bit 7 vale 1 se e solo se almeno uno dei bit 5-0 vale 1, ed il corrispondente bit
nel registro di controllo vale 0.Le condizioni sono le seguenti:
-
stack fault (#IS, bit 6): viene messo a 1 se si cerca di inserire un valore dentro la pila FPU quando è pieno o di togliere un valore dalla pila FPU vuota. Nel caso il bit C1 delle condizioni discrimina i due casi (C1=1 -> pila FPU piena, C1=0 -> pila FPU vuota). Quando questo succede,
viene messo a 1 anche il bit 0 (invalid arithmetic operation).
-
precision (#P, bit 5): il risultato non è rappresentabile con il numero di cifre pari alla precisione
corrente. Non è un gran problema (sono molti i numeri che richiederebbero, ad esempio un numero infinito di cifre)
-
underflow/overflow (#U, bit 4): si è verificata una condizione di underflow. Il risultato è troppo
piccolo in valore assoluto per essere rappresentato in modo normalizzato.
-
overflow (#O, bit 3): si è verificata una condizione di overflow. Il risultato è troppo piccolo
(grande) in valore assoluto per essere rappresentato in modo normalizzato.
-
zero divide (#Z, bit 2): si è tentato di dividere un operando non nullo per zero. Si noti che la divisione 0/0 è un’operazione non valida, che viene gestita da un altro bit.
-
denormalized operand (#D bit 1): un operando dell’operazione è un numero denormalizzato.
-
invalid arithmetic operation (#IA, bit 0): può essere settato in due casi: quando ci sono problemi
con la pila FPU (in congiunzione con il bit 6, #IS), oppure quando si vogliono fare operazioni
numericamente inconsistenti, come +∞ − ∞, ± ∞ ⋅ ±0, ± ∞ / ± ∞, ± 0 / ± 0,
− x , log ( − x) ,
oppure operazioni in cui uno degli operandi è NaN, oppure trasferimenti in cui il destinatario è
troppo piccolo per contenere il risultato.
I flag delle eccezioni sono “appiccicosi” (sticky). Una volta settati rimangono settati finché qualcuno non li cancella. Esistono istruzioni che fanno esattamente questo.
Ci sono poi i bit C3-C0 (14, 10-8): flag delle condizioni. Servono a contenere il risultato di un confronto tra numeri reali (o tra un numero reale ed intero). Sulla base di questi bit si possono poi impostare salti condizionati. Sono l’equivalente dei flag nella FPU. Il registro di stato può essere copiato in AX o in memoria per essere analizzato. In particolare, si possono copiare i flag delle condizioni direttamente nel registro EF della ALU. Ciò consente di usare le istruzioni di salto condizionato della ALU dopo aver eseguito il confronto tra operandi della FPU, come vedremo in seguito.
Figura 3 – registro di controllo della FPU
Il registro di controllo può essere scritto dal programmatore per configurare la FPU. In particolare,
ci sono tre cose che un programmatore può voler fare:
-
intervenire sulla precisione dei conti (bit 9-8): per default, i conti dentro la FPU si fanno su 80
bit (mantissa a 64 bit). Troncamenti/estensioni di operandi avvengono durante il trasferimento
da/verso la memoria, ma dentro la FPU i conti avvengono alla massima precisione. Per compatibilità all’indietro si può cambiare questo default, in modo che le istruzioni della FPU lavorino
su operandi a 32 o 64 bit (cioè con mantissa a 24 o 53 bit). Questo campo non deve essere mai
modificato.
-
Intervenire sul tipo di arrotondamento (bit 11-10): sono possibili le seguenti opzioni:
o
Troncamento (11): si arrotonda sempre verso lo 0
o
Verso l’alto (10): sempre verso +∞
o
Verso il basso (01) sempre verso −∞
o
Al più vicino numero rappresentabile (default, 00).
A futura memoria (servirà per l’esame di Calcolatori Elettronici) conviene ricordare che i linguaggi ad alto livello (ad esempio il C++) impostano la FPU in modo che arrotondi per troncamento, cioè in modo diverso dal suo default. È necessario tenerne conto quando si scrivono programmi misti C++/Assembler che usano la FPU.
-
Mascherare delle eccezioni (bit 5-0): non necessariamente voglio che venga generata
un’eccezione quando si produce un risultato denormalizzato, né che produrre un risultato approssimato generi un’eccezione (è perfettamente normale che ciò accada: il numero 0.1 non è
rappresentabile con un numero finito di cifre in base 2, ad esempio). In altri casi, invece, è più
ragionevole che un’eccezione venga sollevata (ad esempio, se moltiplico per NaN). Impostando
ad 1 alcuni dei bit di questa parte del registro di controllo, si possono mascherare le eccezioni
corrispondenti, cioè fare in modo che non venga eseguito codice quando si verificano. Quando
la FPU viene inizializzata, tutte le eccezioni sono mascherate.
È bene non toccare il registro di controllo, lasciandolo impostato al suo valore di default.
Il comando del debugger GDB:
info float
mostra i registri della FPU in una maniera facilmente comprensibile. In particolare, estrae dal contenuto dei registri di stato e controllo le informazioni significative e le stampa su video in modo
comprensibile.
1.3 Crediti e fonti
Alcune immagini sono state copiate dai seguenti link:
[1] Wikipedia: http://it.wikipedia.org/wiki/IEEE_754
[2] Art of Assembly, http://webster.cs.ucr.edu/AoA/Windows/HTML/RealArithmetic.html
Alcuni esempi sono stati tratti dai seguenti testi:
[3] Intel Architecture Software Developer's Manual, Volume 1: Basic Architecture, chapter 7,
http://www.intel.com/design/pentiumii/manuals/243190.htm
[4] G. Frosini “Architettura dei Calcolatori Volume I – Assembler e Corrispondenza fra C++ e
Assembler”, Aracne.
[5] P.
A.
Carter,
Il
linguaggio
PC
Assembly,
http://www.phatcode.net/res/226/files/pcasm-book-italian.pdf
cap.
6,
2005,
Scarica

Note sulla FPU - Dipartimento di Ingegneria dell`Informazione