Vai al contenuto

Rappresentare i numeri frazionari

Nelle lezioni precedenti abbiamo considerato i numeri interi. Per fare calcoli però è ovviamente necessario rappresentare numeri frazionari. Introduciamo due sistemi per farlo: a virgola fissa e a virgola mobile.

Rappresentazione in virgola fissa

Consideriamo un numero razionale positivo, come \(10.1023\). Questo si può scrivere con la stessa notazione posizionale utilizzata per gli interi, aggiungendo le potenze minori di zero:

\[ A_{10} = \sum_{i = -n}^{m-1} c_i d^i, \]

Nello specifico,

\[ 10.1023 = 1\cdot 10^1 + 0 \cdot10^0 + 1 \cdot10^{-1} + 0\cdot 10^{-2} + 2\cdot 10^{-3} + 4\cdot 10^{-4}. \]

Come noto, spostare il punto decimale a sinistra di una posizione equivale a moltiplicare per \(10\), mentre spostarlo a destra equivale a dividere per \(10\).

Questa rappresentazione è possibile ovviamente anche in base due:

\[ A_{2} = \sum_{i = -n}^{m-1} c_i b^i, \]

e permette di rappresentare frazioni di potenze di due. Vediamo subito che vale:

\[ 10.11_2 = 1\cdot2^1 + 0\cdot2^0 + 1\cdot 2^{-1} + 1\cdot2^{-2} = 2 + \frac{3}{4} = 2.75 \]

Analogamente al caso in base \(10\), spostare il punto (cambiando quindi \(m\) ed \(n\)) equivale a dividere o moltiplicare per \(2\).

\[ 1.011_2 = 1\cdot2^0 + 0\cdot2^{-1} + 1\cdot 2^{-2} + 1\cdot2^{-3} = 1 +\frac{3}{8} = 1.375 \]
\[ 101.1_2 = 1\cdot2^2 + 0\cdot2^1 + 1\cdot 2^0 + 1\cdot2^{-1} = 5 + \frac{1}{2} = 5.5 \]

Se usiamo \(K=n+m\) bit per rappresentare un numero, \(m\) rappresentano la parte intera ed \(n\) la parte frazionaria. Notiamo che questo fissa la precisione con cui posso rappresentare i numeri. Se per esempio usiamo 6 bit, di cui 2 per la parte frazionaria, questa può assumere solo i valori \(\frac{1}{4}\), \(\frac{1}{2}\) e \(\frac{3}{4}\), quindi rappresenterò tutti i numeri tra \(0\) e \(15.75\) a intervalli di \(0.25\). Allocare più bit per la parte frazionaria aumenta la risoluzione, ma diminuisce il range di numeri rappresentabili.

Ovunque scegliamo di mettere il punto, il fatto che abbiamo un numero finito di bit a nostra disposizione fa sì che non sia possibile rappresentare tutte le frazioni. Inoltre, le frazioni rappresentabili non sono le stesse che sono rappresentabili in base 10. Ad esempio, in base 10 non è possibile scrivere il numero \(\frac{1}{3}\) in notazione posizionale, in quanto richiede un numero infinito di cifre: \(0.3333333333\ldots\). Questo succede anche in base 2, sia per \(\frac{1}{3}\), sia per numeri che sono rappresentabili in base 10, come ad esempio \(\frac{1}{5} = 0.2_{10}\). In base 2 abbiamo:

\[ \frac{1}{5} = 0.00110011_2.. = 0.19921875_{10} \]

I numeri in virgola fissa si possono adattare per rappresentare anche numeri negativi, ad esempio scomponendo un numero in parte intera, rappresentata con segno, e parte frazionaria interpretata come potenze negative di due. o in molti altri modi. Non approfondiremo oltre l'argomento, preferendo dedicarci ai numeri in virgola mobile.

Rappresentazione in virgola mobile: IEEE754

I numeri in virgola fissa possono essere molto utili se dobbiamo rappresentare un range preciso di numeri con una risoluzione data (ad esempio tra \(-1\) e \(1\) con una risoluzione di \(2^{-24}\)). Nei problemi scientifici però abbiamo a che fare con diverse scale di grandezza e dobbiamo trovare il modo di rappresentarle senza sprecare memoria. Quello che si fa è usare la notazione scientifica, rappresentando un numero come

\[ V = x\cdot 2^y, \]

dove \(x\) può avere segno negativo. Questa rappresentazione è stata standardizzata negli anni 80 dall' Institute of Electrical and Electronic Engineers (IEEE) con lo standard 754 (IEEE754). Questa standardizzazione ha permesso di uniformare il modo in cui i numeri in virgola mobile sono implementati su computer diversi. Lo standard prevede che i numeri vengano rappresentati come segue:

\[ V = (-1)^S M 2^E \]

dove

  • S è il bit che rappresenta il segno,
  • M, la mantissa rappresenta con \(m\) bits un numero tra \(1\) e \(2 - \epsilon\) oppure tra \(0\) e \(1 -\epsilon\) a seconda del valore dell' esponente,
  • E, l' esponente ci dà la potenza di due da moltiplicare a questo numero, usando \(k\) bit.

In memoria il numero viene rappresentato come segue, con 32 (float) o 64 (double) bit (rappresentazioni con 128 bit sono possibili):

IEEE754

Rappresentazione in memoria dei numeri a virgola mobile IEEE754 a 32bit (float) e 64bit (double).

Immagine per gentile cortesia di L. Rovigatti, Univ. La Sapienza, Roma.

Alcuni possibili combinazioni di bit vengono interpretate come casi speciali: due per lo zero, \(+\infty\), \(-\infty\), e NaN, "Not a Number" per indicare risultati non rappresentabili con numeri reali:

IEEE754_casi_speciali

Rappresentazione in memoria dei casi speciali.

Immagine per gentile cortesia di L. Rovigatti, Univ. La Sapienza, Roma.

Tutti gli altri valori sono rappresentati dalo standard usando due diversi approcci, uno per i numeri \(|V|\geq 1\) (numeri normalizzati) ed uno per i numeri \(|V|<1\) (numeri denormalizzati).

Numeri normalizzati

La rappresentazione standard si ha quando l' esponente non è composto né di tutti 1 né di tutti 0. In questo caso il suo valore viene inteso in rappresentazione in eccesso, cioè si interpretano i \(k\) bit dell'esponente come un intero senza segno e si sottrae un bias:

\[ E = e - \textrm{Bias}, \]

con \(\textrm{Bias} =2^{k-1}-1\) ed \(e\) è il valore intero senza segno rappresentato dai \(k\) bits \(e_{k-1}e_{k-2}\ldots e_2e_1e_0.\) Questo vuol dire che per i float l'esponente può andare da \(-126\) a \(127\) e per i double da \(-1022\) a \(1023\).

( D: Perché abbiamo un numero in meno per gli esponenti negativi ?)

( D: Perché gli esponenenti non vanno da \(-127\) a \(128\) e \(-1023\) a \(1024\)?)

In questo caso, quando l'esponente contiene sia zeri che uni, la mantissa viene interpretata come un intero senza segno di \(m\) bits, che viene sommato a \(1\) (la cosiddetta rappresentazione a 1 implicito). Possiamo farlo visto che l'esponente \(0\) non è comtemplato e possiamo recuperare i numeri \(|V|>1\) semplicemente cambiando l'esponente. Quindi se la mantissa rappresenta il numero \(0.11010\), questo viene interpretato come \(1.11010\). Questo permette di salvare un bit.

Numeri denormalizzati

I numeri "denormalizzati" si usano per rappresentare i numeri vicini allo zero, cioè quelli in cui l'esponente è composto di soli zeri, \(e=0\). In questo caso questo non viene più considerato come \(E= - \textrm{Bias}\), bensì:

\[ E = 1 - \textrm{Bias}, \]

mentre la mantissa viene interpretata senza l'uno implicito. Questo vuol dire che nel caso a 32 bit, ad esempio, l'esponente più piccolo resta \(-126\), ma questo moltiplica un numero compreso tra \(0\) e \(1-\epsilon\) anziché un numero compreso tra \(1\) e \(2-\epsilon\).

Questo cambio di interpretazione di esponente e mantissa permette di avere dei numeri equispaziati vicini allo zero. Facciamo un esempio con un ipotetico formato in virgola mobile a 8 bit, 4 dei quali sono usati per l'esponente, 3 per la mantissa ed uno per il segno. Il Bias è \(2^3 -1 = 7\) e l'esponente può quindi assumere valori da -6 a 7. Quando l'esponente è composto di soli zeri, la mantissa viene interpretata partendo da \(0\) anziché da \(1\), per cui abbiamo:

  • \(s=0\), \(e=0000\), \(M=000\) \(\to\) \(0\cdot 2^{-6} = 0\)
  • \(s=0\), \(e=0000\), \(M=001\) \(\to\) \(\frac{1}{8} \cdot 2^{-6} = \frac{1}{8} \frac{1}{64} = \frac{1}{512}\)
  • \(s=0\), \(e=0000\), \(M=010\) \(\to\) \(\frac{1}{8} \cdot 2^{-6} = \frac{2}{8} \frac{1}{64} = \frac{2}{512}\)
  • \(\ldots\)
  • \(s=0\), \(e=0000\), \(M=111\) \(\to\) \(\frac{1}{8} \cdot 2^{-6} = \frac{7}{8} \frac{1}{64} = \frac{7}{512}\)

I più piccoli valori normalizzati si hanno quando l'esponente vale \(0001\). Abbiamo

  • \(s=0\), \(e=0001\), \(M=000\) \(\to\) \(1 2^{-6} = \frac{1}{64}\)
  • \(s=0\), \(e=0001\), \(M=001\) \(\to\) \(\frac{9}{8} 2^{-6} = \frac{9}{512}\)
  • \(s=0\), \(e=0001\), \(M=010\) \(\to\) \(\frac{10}{8} 2^{-6} = \frac{10}{512}\)
  • \(\ldots\)
  • \(s=0\), \(e=1111\), \(M=000\) \(\to\) \(\infty\)

Due considerazioni importanti

Ci sono due importanti considerazioni da fare.

  1. Notiamo che appena abbiamo \(e > 1-2^{k-1}\) i numeri non sono più equispaziati: ogni volta che incrementa l'esponente viene incrementata anche la loro spaziatura!
  2. Notiamo che se interpretiamo la rappresentazione in bit (con \(s=0\)) come quella di un intero senza segno, questi sono disposti in ordine crescente.

Arrotondamenti

I numeri che rappresentiamo sono ovviamente solo i razionali, e solo con un numero limitato di cifre. Ogni volta che scriviamo un numero in base 10, o come frazione, o che il risultato di un operazione è un numero irrazionale, questo viene arrotondato al più vicino numero razionale rappresentabile in formato IEEE754. Lo standard prevede quattro diversi modi di arrotondare, ma i linguaggi che useremo ne adoperano uno solo, cioè quello al numero più vicino. Per cui:

  • \(1.4 \to 1\)
  • \(1.5 \to 2\)
  • \(-1.6 \to -2\)

Proprietà delle operazioni

Le operazioni vengono eseguite così come si eseguono in notazione scientifica: prima si eguagliano gli esponenti, e poi si opera sulle mantisse. Ad esempio, per somme e sottrazioni:

\[ 83.5 + 0.7 = 0.835 \cdot 10^2 + 0.007\cdot 10^2 = (0.835 +0.007)\cdot 10^2 = 0.842 \cdot 10^2 = 84.2 \]

Quando operiamo con un numero finito di bit dobbiamo tener conto che possiamo perdere dell' informazione. Il risultato delle operazioni arriva dopo l'operazione di arrotondamento:

\[ a + b = \textrm{Round}(a+b) \]

Questo vuol dire che anche se operazioni come la somma soddisfano la proprietà commutativa, \(a+b = b+a\), esse non soddisfano la proprietà associativa:

\[ (a+b) + c \neq a + (b+c) \]

in quanto questa risulta essere

\[ \textrm{Round}(a+b) + \textrm{Round}(c) \neq \textrm{Round}(a) + \textrm{Round}(b+c) \]

Ad esempio, usando numeri double abbiamo:

\[ (1e^{16} + 0.001) - 1e^{16} = 0.0. \]

Per questi motivi bisogna stare molto attenti all'ordine delle operazioni, ed in particolare alle sottrazioni tra numeri molto grandi e molto simili, cercando di ricondurle ad operazioni tra numeri più piccoli. Un esempio tipico è \(x^2 - y^2\), il cui errore di arrotondamento è minore se viene calcolata come \((x+y)(x-y)\).