L’insieme dei numeri reali è un insieme infinito e denso; denso significa che presi due numeri qualsiasi esistono sempre altri numeri compresi tra essi.
I numeri reali rappresentabili in memoria sono però solo un sottoinsieme finito e discreto; ciò significa che si possono rappresentare solo i numeri all’interno di un certo intervallo e che anche all’interno di quell’intervallo esistono dei numeri non rappresentabili. Si verifica inoltre che i numeri rappresentabili non sono distribuiti in modo uniforme ma risultano più densi vicino allo zero.
Standard IEEE 754
Lo standard IEEE 754 definisce un metodo per la rappresentazione dei numeri in virgola mobile, o floating point.
Il numero reale viene dapprima rappresentato in binario, convertendo opportunamente la parte intera e la parte frazionaria.
Il numero binario così ottenuto viene trasformato in notazione esponenziale normalizzata cioè nella forma ±1,M x 2E
. Le cifre dopo la virgola (M
) vengono chiamate mantissa, il simbolo E
rappresenta l’esponente.
La forma generale della notazione esponenziale normalizzata in una base qualsiasi è ±1,M x bE
; quindi in base 10 si ha 1,M x 10E
; per esempio, il numero 1000 potrebbe essere scritto come 1,0 x 103 e il numero 0,01 potrebbe essere scritto come 1,0 x 10-2).
Usando la notazione esponenziale normalizzata, per identificare un numero sono sufficienti tre informazioni:
-
il segno del numero,
-
la mantissa,
-
l’esponente a cui elevare la base, con relativo segno.
Avendo queste informazioni si può risalire al numero rappresentato con il calcolo (-1)S x M x 2E
dove S
è il segno (0 rappresenta il segno positivo, 1 il segno negativo e quindi -10=1 e -11=-1).
Lo standard IEEE 754 prevede che un numero reale floating point sia rappresentato con 4 byte (precisione singola):
-
1 bit per il segno,
-
8 bit per l’esponente,
-
23 bit per la mantissa.
Inoltre definisce un formato esteso (double) di 8 byte (precisione doppia):
-
1 bit per il segno,
-
11 bit per l’esponente,
-
52 bit per la mantissa.
Questa notazione è adatta per le operazioni di confronto tra numeri:
-
per primo c’è il segno per vedere se si tratta di numeri positivi o negativi;
-
l’esponente è indicato prima della mantissa perché numeri con esponente più grande sono più grandi, indipendentemente dalla mantissa.
I bit dell’esponente vengono utilizzati per rappresentare sia esponenti positivi che negativi.
Per conservare l’ordinamento anche tra gli esponenti negativi e positivi, per la rappresentazione dell’esponente viene usato un metodo particolare (esponente biased).
L’esponente viene rappresentato nella forma bias + E dove il valore del bias dipende dal numero di bit usati per l’esponente; se i bit dell’esponente sono n il bias è 2n/2 - 1
(per 8 bit è 127, per 11 bit è 1023).
Il numero di bit riservati all’esponente determina l’intervallo dei numeri ammessi o ordine di grandezza o range. Se l’esponente positivo è troppo grande si verifica un overflow, se l’esponente negativo è in valore assoluto troppo grande si verifica un underflow.
I valori vanno da 1,18 x 10-38 a 3,40 x1038 in precisione singola e da 2,23 x 10-308 a 1,79 x10308 in precisione doppia.
Il numero di bit riservati alla mantissa determina il numero di cifre significative che si possono usare, cioè la precisione del numero. In realtà la cifra 1 prima della virgola viene sottintesa e quindi si hanno 24 bit di mantissa in precisione singola (6-7 cifre decimali) e 53 in precisione doppia (15-16 cifre decimali) e il numero viene calcolato come (-1)S x (1+M) x 2E
.
Precisione singola
Il bias è 127.
Il valore 127 corrisponde allo 0 come esponente effettivo.
Tutti gli esponenti negativi sono rappresentati dai valori compresi tra 1 e 126 e quindi hanno il primo bit 0.
Tutti gli esponenti positivi sono rappresentati dai valori compresi tra 128 e 255 e quindi hanno il primo bit 1.
L’ordine di grandezza dei numeri rappresentabili va da 2-127 a 2128 cioè da 1,18 x 10-38 a 3,40 x1038.
La precisione della mantissa è di 24 cifre binarie cioè 6-7 cifre decimali significative.
Per ottenere la rappresentazione floating point in precisione singola del numero decimale ‑109,78125 bisogna convertire in binario la parte intera e la parte decimale ottenendo:
1101101,11001
che in notazione normalizzata corrisponde a:
1,10110111001x26
All’esponente va aggiunto il valore del bias 127 e quindi l’esponente è costituito dal valore 33:
10000101
Poiché il numero è negativo il bit del segno va impostato a 1.
Si ottengono quindi i valori:
1 per il segno 10000101 per l’esponente 10110111001 completato con zeri per la mantissa (l’1 della parte intera è sottinteso)
e quindi la rappresentazione finale è
11000010110110111001000000000000
Data la rappresentazione floating point in precisione singola
01000010100110100000000000000000
bisogna distinguere le varie parti:
0 1 bit per il segno 10000101 8 bit per l’esponente 00110100000000000000000 i rimanenti per la mantissa (con sottintesa la parte intera a 1)
L’esponente usa come bias 127 (01111111
) e quindi il valore dell’esponente è
133 (10000101
) -127 = 6
Il numero rappresentato è quindi 1,001101x26
cioè 1001101
, cioè 77.
Precisione doppia
Il bias è 1023.
Il valore 1023 corrisponde allo 0 come esponente effettivo;
L’ordine di grandezza dei numeri rappresentabili va da 2-1023 a 21024 cioè da 2,23 x 10-308 a 1,79 x 10308.
La precisione della mantissa è di 53 cifre binarie cioè 15-16 cifre decimali significative.
Lo standard IEEE 754 definisce inoltre alcuni valori speciali:
-
il numero 0,0 viene rappresentato da una sequenza di bit tutti a 0 (sia per il segno, che per l’esponente, che per la mantissa); il valore 0 riservato all’esponente fa in modo che non venga aggiunto 1 alla mantissa;
-
il valore infinito viene rappresentato con tutti i bit della mantissa a 0 e con tutti i bit dell’esponente a 1; l’infinito viene considerato positivo o negativo secondo il bit di segno del numero;
-
viene definito un valore NaN (Not a Number); ce ne sono due tipi: quello che, ottenuto come risultato di un’operazione, genera un errore (rappresentato con una mantissa con tutti i bit a 1 tranne il più significativo) e quello che non genera errori (mantissa con tutti i bit a 0 tranne il più significativo).
Operazioni con la rappresentazione floating point
Si possono sommare due numeri floating point solo se hanno lo stesso esponente.
Se l’esponente è uguale si sommano le mantisse e si mantiene l’esponente comune.
Se gli esponenti non sono uguali bisogna trasformare il numero con esponente più grande per rendere l’esponente uguale a quello dell’altro numero; in tal caso il numero è in forma non normalizzata; il risultato poi deve essere normalizzato.
Per eseguire la moltiplicazione si moltiplicano le mantisse e si sommano gli esponenti (perché la base è la stessa); il prodotto è in forma non normalizzata e aumenta il numero di cifre e quindi deve essere arrotondato (nearest rounding).
Per l’arrotondamento si adotta il seguente metodo:
-
se la prima cifra in più è 0 si aggiunge 0 all’ultima cifra permessa;
-
se la prima cifra in più è 1 si aggiunge 1 all’ultima cifra permessa.
Se le cifre permesse dopo la virgola sono 8 si ha che:
1,000000000001
viene arrotondato a 1,00000000
1,000000001111
viene arrotondato a 1,00000001
Errori dovuti alla rappresentazione floating point
La precisione limitata e gli arrotondamenti nei risultati delle operazioni causano alcuni problemi.
Le operazioni di confronto possono dare risultati inaspettati e inoltre non valgono più le proprietà commutativa, associativa e distributiva (è quindi importante l’ordine di esecuzione delle operazioni).
Per esempio se in un programma si fa un ciclo che sommi 10 volte il numero 0,1 ci si aspetta che il risultato faccia 1, ma non è così (confrontando il risultato con il valore 1 si vede che non è uguale); questo perché non esiste una rappresentazione precisa per il numero 0,1.
Gli errori di arrotondamento possono influire talmente sul risultato da renderlo inaccettabile; esiste una vera e propria teoria sulla propagazione degli errori (analisi numerica). In realtà, anziché determinare quanto grande è l’errore nel risultato per effetto degli errori dovuti all’approssimazione iniziale, si cerca di determinare con quale approssimazione si devono considerare i dati iniziali, se si vuole mantenere entro un certo limite l’errore nel risultato.