Assembly Avanzato con MASM

Capitolo 16: Rappresentazione dei numeri reali con la CPU


Secondo i matematici della scuola pitagorica, l'essenza della natura era completamente rappresentata dai numeri interi positivi e dallo zero; in base a quella filosofia di pensiero, qualsiasi grandezza (lunghezze, aree, volumi, etc) era esprimibile sotto forma di un numero intero o, al più, di un rapporto tra due numeri interi (quello che in matematica si definisce insieme dei numeri razionali).
I pitagorici avevano scoperto che un qualsiasi numero razionale, espresso in base \(10\), presentava la caratteristica di essere periodico; nella sua parte decimale cioè, era sempre presente una sequenza finita di cifre che si ripeteva all'infinito. Il numero \(1\), ad esempio, ha come parte decimale lo zero ripetuto all'infinito, per cui può essere scritto simbolicamente come \(1,\overline{0}\); analogamente, si ha \(1/3=0,\overline{3}\) o anche \(3/4=0,75\overline{0}\).
Si narra che Ippaso di Metaponto, anch'egli esponente della scuola pitagorica, ebbe l'idea di disegnare sulla sabbia un quadrato di lato \(l = 1\), per poi tentare di calcolarne la diagonale \(d\), nella speranza di ottenere un numero razionale come risultato. Si tratta in pratica di applicare il Teorema di Pitagora per determinare l'ipotenusa di un triangolo rettangolo isoscele i cui due cateti hanno entrambi lunghezza \(l = 1\); se abbiamo a disposizione una calcolatrice ad elevata precisione, otteniamo un risultato (approssimato) che ci fa venire dei seri dubbi sulla sua razionalità: \[ d^2 = l^2 + l^2 = 1^2 + 1^2 = 2 \implies d = \sqrt{2} = 1,4142135623730950488016887 ... \] La moderna matematica ci permette di dimostrare facilmente che \(\sqrt{2}\), in effetti, non è un numero razionale!
Partiamo dal fatto che un numero razionale è un rapporto tra due numeri interi \(a\) e \(b\), con \(b\) diverso da zero; inoltre, se \(a\) e \(b\) sono entrambi pari, possiamo dividerli ripetutamente per \(2\) finché uno di essi non diventa dispari (ad esempio, \(6 / 4 = 3 / 2\)). Assumendo che \(\sqrt{2}\) sia un numero razionale, possiamo scrivere quindi: \[ {a \over b} = \sqrt{2} \implies {a^2 \over b^2} = 2 \implies a^2 = 2b^2 \] Ma se \(a^2\) è il doppio di \(b^2\), allora \(a^2\) è un numero pari; inoltre, siccome il quadrato di un numero pari è pari e il quadrato di un numero dispari è dispari, anche \(a\) è un numero pari. Per il ragionamento fatto in precedenza, \(b\) deve essere allora un numero dispari.
Se \(a\) è pari, possiamo trovare un numero intero \(c\), non nullo, tale che \(a = 2c\); sostituendo nel risultato precedente si ottiene quindi: \[ a^2 = 2b^2 \implies ({2c})^2 = 2b^2 \implies 4c^2 = 2b^2 \implies 2c^2 = b^2 \] Risulta cioè che \(b^2\) è il doppio di \(c^2\) e quindi è un numero pari, così come lo deve essere allora anche \(b\); ma tutto ciò è impossibile visto che \(b\) non può essere allo stesso tempo pari e dispari. Con un classico ragionamento per assurdo abbiamo quindi dimostrato che \(\sqrt{2}\) non può essere espresso come rapporto tra due numeri interi!

Anche Ippaso, attraverso calcoli ben più complicati, si accorse con grande sconcerto che la parte frazionaria di \(d\) era formata da una sequenza interminabile di cifre che non sembrava presentare alcuna periodicità; rivelò allora questa scoperta agli altri pitagorici, ma si sentì rispondere di tenere la bocca chiusa per evitare che questa "scandalosa" verità venisse a galla!
In pratica, Ippaso si era imbattuto nei cosiddetti numeri irrazionali; si tratta di numeri (come \(\sqrt{2}, \pi, \exp\)) che non sono interi e non possono essere espressi neppure sotto forma di rapporto tra numeri interi.
Secondo la leggenda, Ippaso non mantenne il segreto e per questo venne affogato in mare!

16.1 Principali insiemi numerici della matematica

In matematica, la totalità dei numeri interi positivi forma l'insieme dei numeri naturali, indicato con il simbolo \(\Bbb N\); questo nome deriva dal fatto che tali numeri vengono appresi in modo naturale, sin da bambini, quando si impara a contare.
Nell'insieme \(\Bbb N\) solo l'addizione e la moltiplicazione sono operazioni "ben definite" (o interne); infatti, addizionando o moltiplicando due numeri interi positivi, si ottiene come risultato un numero intero positivo. Se proviamo ad effettuare una sottrazione tra due numeri interi positivi, vediamo però che tale operazione è possibile solo se il minuendo è strettamente maggiore del sottraendo; aggiungendo lo zero all'insieme \(\Bbb N\), diventa possibile anche la sottrazione tra due numeri interi positivi uguali (\(n-n=0\)).
Si rende necessario quindi estendere l'insieme \(\Bbb N\) in modo che sia possibile la sottrazione tra due numeri interi positivi qualunque; ciò viene ottenuto definendo l'insieme dei numeri interi relativi, indicato con il simbolo \(\Bbb Z\) e costituito dalla totalità dei numeri interi positivi, negativi e dallo zero. Fissata una retta, un suo punto di riferimento (che rappresenta lo \(0\)) e una distanza unitaria \(U\), tutti gli infiniti punti alla destra dello \(0\), posti ad intervalli di \(U\), rappresentano nell'ordine i numeri positivi \(+1, +2, +3\), etc; analogamente, tutti gli infiniti punti alla sinistra dello \(0\), posti ad intervalli di \(U\), rappresentano nell'ordine i numeri negativi \(-1, -2, -3\), etc. La Figura 16.1 illustra questa situazione. Calcolare, ad esempio, \(2-5\), significa partire dallo \(0\), andare verso destra di \(2\) punti e poi andare verso sinistra di \(5\) punti; alla fine ci ritroviamo al punto indicato con \(-3\), per cui \(2-5=-3\).
Risulta evidente che l'insieme \(\Bbb N\) è interamente contenuto nell'insieme \(\Bbb Z\); in simboli matematici, \(\Bbb N \subset \Bbb Z\).
Nell'insieme \(\Bbb Z\) l'addizione, la sottrazione e la moltiplicazione sono operazioni ben definite; si presenta però il problema della divisione che risulta possibile solo quando il numeratore è un multiplo intero del denominatore. Si rende necessario quindi estendere l'insieme \(\Bbb Z\) in modo che sia possibile la divisione tra due numeri interi relativi qualunque; ciò viene ottenuto definendo l'insieme dei numeri razionali, indicato con il simbolo \(\Bbb Q\) e costituito dalla totalità dei numeri frazionari distinti che si ottengono dividendo tra loro due numeri interi relativi \(a\) e \(b\), con \(b\) diverso da zero.
Risulta evidente che l'insieme \(\Bbb Z\) è interamente contenuto nell'insieme \(\Bbb Q\), dato che ogni intero relativo \(n\) può essere scritto come \(n/1\); in simboli matematici, \(\Bbb Z \subset \Bbb Q\).
Come scoperto però da Ippaso da Metaponto, l'insieme \(\Bbb Q\) non risolve tutti i problemi in quanto esistono numeri che non sono interi e non possono essere espressi neppure come rapporto tra due numeri interi; tali numeri formano l'insieme dei numeri irrazionali, indicato con il simbolo \(\Bbb I\).
Unendo l'insieme dei numeri razionali con quello dei numeri irrazionali, si ottiene l'insieme dei numeri reali, indicato con il simbolo \(\Bbb R\); in simboli matematici, \(\Bbb R = \Bbb Q \cup \Bbb I\).
Tornando alla rappresentazione di \(\Bbb Z\) sulla retta (Figura 16.1), si può notare che tra due numeri relativi consecutivi \(m\) e \(m+1\), esistono infiniti punti della retta stessa; non è possibile quindi stabilire una corrispondenza biunivoca tra i numeri relativi e i punti di una retta. Lo stesso discorso vale anche per l'insieme \(\Bbb Q\); in matematica si dimostra che i punti della retta non associabili ad alcun numero razionale rappresentano proprio i numeri irrazionali e l'insieme di tutti i numeri, razionali e irrazionali, copre quindi l'intera retta.
Si può stabilire così una corrispondenza biunivoca tra i numeri reali e i punti di una retta; non esistono punti di una retta non rappresentabili con un numero reale, per cui, ad ogni punto di una retta corrisponde un numero reale e viceversa. Ciò è possibile perché, tra due numeri reali qualunque, per quanto vicini possano essere, ne esistono infiniti altri (proprio come accade per i punti di una retta); si dice allora che \(\Bbb R\) è un insieme non numerabile, nel senso che non è possibile mettere in qualsiasi ordine i numeri reali come si fa, ad esempio, con i numeri interi positivi (\(1, 2, 3\), etc).

Una ulteriore estensione di \(\Bbb R\) si rende necessaria affinché siano possibili operazioni come, ad esempio, il logaritmo di un numero negativo; ciò si ottiene introducendo l'insieme dei numeri complessi, indicato con il simbolo \(\Bbb C\) e costituito da tutte le possibili coppie ordinate \((a, b)\) di numeri reali.
Definendo l'unità immaginaria \(i\), tale che \(i^2 = -1\) e quindi \(i = \sqrt{-1}\), un numero complesso \(z\) può essere scritto in forma algebrica come \(z = a + ib\); si dice che \(a\) è la parte reale di \(z\) e \(b\) è il coefficiente dell'immaginario (in simboli, \(a=Re(z), b=Im(z)\)).
Si vede subito che \(\Bbb R\) è totalmente contenuto in \(\Bbb C\) (in simboli \(\Bbb R \subset \Bbb C\)); infatti, posto \(b = 0\), si ottengono tutti i numeri reali in forma di coppie ordinate \((a,0)\).

16.2 Codifica dei numeri interi con la CPU

Nel tutorial Assembly Base abbiamo visto che la CPU lavora con due livelli di tensione elettrica (basso, alto) che possiamo associare ai due simboli \(0\) e \(1\); si tratta degli unici due simboli che formano il sistema binario o sistema di numerazione posizionale in base \(2\).
Su una architettura a \(n\) bit, possiamo codificare un sottoinsieme finito di \(\Bbb N\) più lo zero, costituito da tutti i numeri interi positivi compresi tra \(0\) e \(2^n-1\); a tale proposito, ci serviamo della sequenza consecutiva e contigua di numeri binari rappresentabili con \(n\) bit, a partire da \(0\). Nel caso, ad esempio, di una architettura a \(8\) bit, abbiamo una sequenza di numeri binari che va da \(00000000b\) a \(11111111b\); tale sequenza viene utilizzata per codificare tutti i numeri interi positivi da \(0\) a \(2^8-1=255\).
La CPU non ha la più pallida idea di cosa sia un numero intero positivo, ma abbiamo visto che attraverso apposite reti logiche, le codifiche binarie di quei numeri stessi possono essere usate come operandi a \(n\) bit sui quali eseguire, via hardware, operazioni elementari come addizioni, sottrazioni, moltiplicazioni e divisioni; inoltre, abbiamo anche visto che quelle stesse operazioni possono essere eseguite via software su operandi di ampiezza qualunque attraverso appositi algoritmi che scompongono tali numeri in gruppi di \(n\) bit.

Grazie poi ad un effetto collaterale dell'aritmetica modulare, sappiamo che la CPU è in grado di codificare via hardware anche un sottoinsieme finito di \(\Bbb Z\); in questo caso abbiamo visto che, su una architettura a \(n\) bit, possiamo codificare tutti i numeri interi con segno compresi tra \(-(2^n/2)\) e \(+((2^n/2)-1)\). A tale proposito, ci serviamo della sequenza consecutiva e contigua di numeri binari rappresentabili con \(n\) bit, a partire da \(1000000...0b\) (in questo modo, i numeri negativi hanno il bit più significativo che vale \(1\), mentre i numeri positivi hanno il bit più significativo che vale \(0\)); nel caso, ad esempio, di una architettura a \(8\) bit, abbiamo una sequenza di numeri binari che va da \(10000000b\) a \(01111111b\) e tale sequenza viene utilizzata per codificare tutti i numeri interi con segno da \(-(2^8/2)=-128\) a \(+((2^8/2)-1)=+127\).
Valgono tutte le considerazioni esposte in precedenza sulle operazioni eseguibili via hardware sui numeri interi con segno a \(n\) bit e via software sui numeri interi con segno di ampiezza qualunque.

16.3 Codifica dei numeri reali con la CPU

Come abbiamo appena visto, la CPU è in grado di operare via hardware su un sottoinsieme finito di numeri interi, con o senza segno; ciò permette di eseguire su tali numeri, operazioni elementari ad altissima velocità.
Ben diverso è il discorso quando si intende operare sui numeri reali; in tal caso, infatti, in assenza di alternative si è costretti a scrivere complessi algoritmi che comportano una elaborazione relativamente lenta da parte della CPU. Proprio per alleviare questo problema, sin dai tempi delle vecchie CPU 8086, la Intel ha introdotto un microprocessore supplementare denominato coprocessore matematico o FPU (Floating Point Unit); la FPU verrà trattata nel capitolo successivo, mentre in questo capitolo ci occuperemo degli aspetti teorici che stanno alla base della rappresentazione dei numeri reali con una normale CPU.

Un qualunque numero reale \(x\) può essere scritto nella forma: \[ x = m b^e \] detta forma normalizzata in base \(b\) di \(x\); per "forma normalizzata" si intende il fatto che \(x\) risulta essere del tipo \(\pm(0,...) \cdot b^e\), con la prima cifra dopo la virgola diversa da zero (se il numero non è nullo).
In questa formula, \(b\) è la base del sistema di numerazione usato, mentre \(e\) è la caratteristica e rappresenta un numero intero con segno; \(m\) è la mantissa e per quanto appena visto deve essere tale che: \[ {1 \over b} \le \vert m \vert \lt 1 \] Indicando con \(c_1, c_2, c_3, ...\) le cifre di \(m\), si ha: \[ m = c_1 b^{-1} + c_2 b^{-2} + c_3 b^{-3} + ... \] Ad esempio, se \(x = 245,65134\), abbiamo in base \(b=10\): \[ m = 2 \cdot 10^{-1} + 4 \cdot 10^{-2} + 5 \cdot 10^{-3} + 6 \cdot 10^{-4} + 5 \cdot 10^{-5} + 1 \cdot 10^{-6} + 3 \cdot 10^{-7} + 4 \cdot 10^{-8} = 0,24565134 \] Siccome la parte intera di \(x\) è \(245\), dobbiamo moltiplicare \(m\) per \(1000 = 10^3\), per cui \(e=3\); si ha quindi: \[ x = m \cdot b^e = 0,24565134 \cdot 10^3 = 245,65134 \] Viceversa, sapendo che \(b=10\), \(m=-0,153428\), \(e=-2\), ricaviamo facilmente: \[ x = m \cdot b^e = -0,153428 \cdot 10^{-2} = -0,00153428 \] Per eseguire addizioni o sottrazioni tra due numeri reali in forma normalizzata, bisogna prima riportare i due numeri alla stessa caratteristica; ciò equivale, infatti, ad incolonnare le parti intere e le parti frazionarie dei due numeri. La regola prevede che venga presa la caratteristica maggiore tra le due; una volta compiuto questo passo, si tratta di eseguire l'operazione (addizione o sottrazione) sulle mantisse, per cui ci riconduciamo a addizioni e sottrazioni tra numeri interi che già abbiamo studiato nel tutorial Assembly Base.
Dati quindi \(x_1=m_1 \cdot b^{e_1}\) e \(x_2=m_2 \cdot b^{e_2}\), con \(e_1=e_2=e\), si ha: \[ x_1 \pm x_2 = (m_1 \cdot b^e) \pm (m_2 \cdot b^e) = (m_1 \pm m_2) \cdot b^e \] Supponiamo, ad esempio, di voler sommare i due numeri reali seguenti, espressi in base \(b=10\): \[ x_1=m_1 \cdot b^{e_1} = -0,15478329 \cdot 10^3 = -154,78329 \\ x_2=m_2 \cdot b^{e_2} = +0,61873214 \cdot 10^5 = +61873,214 \] Portando \(x_1\) a caratteristica \(5\) otteniamo \(x_1=-0,0015478329 \cdot 10^5\); a questo punto sommiamo le due mantisse ottenendo: \begin{align} & -0,0015478329 + \cr & +0,6187321400 = \cr \hline & +0,6171843071 \end{align} Il risultato finale è \(+0,6171843071 \cdot 10^5\) che è già in forma normalizzata per cui non necessita di alcuna modifica della caratteristica.

La moltiplicazione tra due numeri reali in forma normalizzata è più semplice di quanto si possa pensare; infatti, dati \(x_1=m_1 \cdot b^{e_1}\) e \(x_2=m_2 \cdot b^{e_2}\), si ha: \[ x_1 \cdot x_2 = (m_1 \cdot b^{e_1}) \cdot (m_2 \cdot b^{e_2}) = (m_1 \cdot m_2) \cdot (b^{e_1} \cdot b^{e_2}) = (m_1 \cdot m_2) \cdot b^{e_1 + e2} \] La caratteristica del risultato quindi è la somma delle due caratteristiche, mentre la mantissa è il prodotto delle due mantisse; in sostanza, ci riconduciamo a somme e prodotti tra numeri interi che già abbiamo studiato nel tutorial Assembly Base.

Ad esempio, se \(x_1 = 0,31479614 \cdot 10^{-2}\) e \(x_2 = 0,94173212 \cdot 10^5\), in base \(b=10\), si ha: \[ x_1 \cdot x_2 = (0,31479614 \cdot 0,94173212) \cdot 10^{(-2 + 5)} = 0,29645363629 \cdot 10^3 \] Il risultato finale è già in forma normalizzata per cui non necessita di alcuna modifica della caratteristica.

Analoghe considerazioni per la divisione tra due numeri reali in forma normalizzata (con il secondo numero diverso da zero); dati \(x_1=m_1 \cdot b^{e_1}\) e \(x_2=m_2 \cdot b^{e_2}\), si ha: \[ {x_1 \over x_2} = {{m_1 \cdot b^{e_1}} \over {m_2 \cdot b^{e_2}}} = {m_1 \over m_2} \cdot {b^{e_1} \over b^{e_2}} = {m_1 \over m_2} \cdot b^{e_1 - e2} \] La caratteristica del risultato quindi è la differenza tra le due caratteristiche, mentre la mantissa è il quoziente tra le due mantisse; in sostanza, ci riconduciamo a sottrazioni e divisioni tra numeri interi che già abbiamo studiato nel tutorial Assembly Base.

Ad esempio, se \(x_1 = -0,61895473 \cdot 10^2\) e \(x_2 = +0,18567488 \cdot 10^{-3}\), in base \(b=10\), si ha: \[ {x_1 \over x_2} = {-0,61895473 \over +0,18567488} \cdot 10^{(2-(-3))} = -3,33354048754 \cdot 10^5 \] Il risultato finale non è in forma normalizzata per cui è necessario spostare di un posto verso destra le cifre della mantissa, aumentando quindi di \(1\) la caratteristica; otteniamo così il numero reale \(-0,333354048754 \cdot 10^6 \).

16.3.1 Codifica binaria dei numeri reali

Le considerazioni appena esposte hanno un valore puramente teorico; infatti, non avendo posto dei limiti all'ampiezza della caratteristica e della mantissa, risulta possibile rappresentare in forma normalizzata qualsiasi numero reale, con precisione pressoché infinita.
Il discorso cambia nel momento in cui vogliamo codificare in binario i numeri reali in forma normalizzata; in tal caso, infatti, dobbiamo tenere conto dei limiti fisici dei registri o delle locazioni di memoria che utilizziamo per contenere la mantissa e la caratteristica.
La cosa più ovvia da fare consiste allora nello stabilire una serie di regole; in sostanza, dobbiamo decidere quanti bit riservare alla codifica dei numeri reali in forma normalizzata e come ripartire tali bit tra mantissa e caratteristica. Possiamo decidere, ad esempio, di utilizzare una codifica a \(32\) bit riservando i \(24\) bit da \(0\) a \(23\) alla mantissa, i \(7\) bit da \(24\) a \(30\) alla caratteristica e il bit \(31\) al segno della mantissa secondo la solita convenzione (\(0 =\) segno positivo, \(1 =\) segno negativo). Si perviene allora alla situazione illustrata in Figura 16.2. Da quanto abbiamo visto in precedenza, la mantissa, composta dalle cifre binarie \(c_0, c_1, c_2, ..., c_{23}\) (a partire dalla meno significativa), deve essere interpretata come: \[ m = c_{23} 2^{-1} + c_{22} 2^{-2} + c_{21} 2^{-3} + ... + c_{0} 2^{-24} \] La caratteristica è un numero intero con segno in complemento a \(2\), modulo \(2^7=128\) e rappresenta l'esponente \(e\) a cui bisogna elevare la base \(2\); quindi, la codifica di Figura 16.2 rappresenta il numero reale: \[ x = (-1)^S \cdot (c_{23} 2^{-1} + c_{22} 2^{-2} + c_{21} 2^{-3} + ... + c_{0} 2^{-24}) \cdot 2^e \] Supponiamo di voler codificare il numero reale \(x = 3,625\). La parte intera è \(3\), che in binario si scrive \(11_2\), mentre la parte frazionaria è \(0,625\), che in binario si scrive \(0,101_2\); infatti: \[ 0,101_2 = 1 \cdot 2^{-1} + 0 \cdot 2^{-2} + 1 \cdot 2^{-3} = 1 \cdot 0,5 + 0 \cdot 0,25 + 1 \cdot 0,125 = 0,5 + 0 + 0,125 = 0,625 \] Si ha quindi \(x=11,101_2\), che normalizzato diventa \(0,11101_2\); estendendo tale numero binario a \(24\) bit si ottiene la mantissa \(m=11101000000000000000000\)\(0_2\) (ovviamente, trattandosi di un numero decimale minore di \(1\), lo possiamo estendere a \(24\) bit aggiungendo zeri non significativi alla sua destra). Il segno della mantissa è positivo, per cui \(S=0\).
La caratteristica è \(10_2=2\) in quanto dobbiamo far scorrere le cifre della mantissa di due posti verso sinistra; in definitiva, abbiamo \(x=0,11101_2 \cdot 2^{10_2}\), che codificato in binario diventa: \[ x = 0 \vert 0000010 \vert 111010000000000000000000 \] Viceversa, supponiamo di avere la codifica binaria: \[ x = 1 \vert 1111110 \vert 111001110100000000000000 \] La mantissa (eliminando gli zeri non significativi a destra) è \(1110011101_2\) e il suo segno è negativo (\(S=1\)); si ha quindi: \begin{align} m &= -0,1110011101_2 \\ &= -(1 \cdot 2^{-1} + 1 \cdot 2^{-2} + 1 \cdot 2^{-3} + 0 \cdot 2^{-4} + 0 \cdot 2^{-5} + 1 \cdot 2^{-6} + 1 \cdot 2^{-7} + 1 \cdot 2^{-8} + 0 \cdot 2^{-9} + 1 \cdot 2^{-10}) \\ &= -0,9033203125 \end{align} La caratteristica, in complemento a \(2\) modulo \(128\), codifica il numero negativo \(-2\), per cui si ha, in definitiva: \[ x = -0,9033203125 \cdot 2^{-2} = -0,9033203125 / 4 = -0,225830078125 \] Si può notare che, nella rappresentazione dei numeri reali in forma normalizzata, la posizione della virgola varia al variare della caratteristica; si parla allora di numeri in virgola mobile o floating point numbers.

16.3.2 Caratteristiche dei numeri in virgola mobile a n bit

Nel caso generale, consideriamo una codifica a \(n\) bit, con un bit destinato al segno, \(k\) bit destinati alla mantissa e \(h\) bit destinati alla caratteristica; si ha quindi: \[ n = k + h + 1 \] Innanzi tutto, si vede subito che non è possibile codificare alcun numero irrazionale in forma normalizzata; infatti, avendo a disposizione solo \(k\) bit per la mantissa, risulta finito e limitato il numero di cifre dopo la virgola. Sarà possibile codificare esclusivamente un sottoinsieme finito e limitato dei numeri razionali; analizziamo quindi le caratteristiche di tale sottoinsieme.
La mantissa, per come è stata definita, deve essere tale che: \[ 1 \cdot 2^{-1} \le \vert m \vert \le 1 \cdot 2^{-1} + 1 \cdot 2^{-2} + 1 \cdot 2^{-3} + ... + 1 \cdot 2^{-k} \] La caratteristica deve essere tale che: \[ -(2^h / 2) \le e \le +((2^h / 2) - 1) \] In pratica, come è stato appena spiegato, con questo sistema possiamo codificare un sottoinsieme finito e limitato dei numeri razionali; inoltre, all'interno di tale sottoinsieme, determinati numeri razionali non risultano codificabili. Osserviamo, infatti, che mentre per la parte intera non c'è alcun problema, la parte frazionaria risulterà invece codificabile solo se essa è esprimibile sotto forma di somma di potenze intere di \(2\); ad esempio, in precedenza abbiamo visto che \(x = 3,625\) è codificabile in quanto la parte frazionaria \(0,625\) è esprimibile come \(0,101_2\).
Tutto ciò equivale ad affermare che, dato un numero razionale \(x\), espresso come rapporto tra due numeri interi relativi \(a\) e \(b\), con \(b \neq 0\), tale numero risulterà codificabile in binario se e solo se il suo denominatore è una potenza intera di \(2\); ad esempio, \(1 / 3\) non è codificabile in binario in quanto \(3\) non è una potenza intera di \(2\).

Siccome il nostro sottoinsieme di numeri razionali codificabili in binario è finito, esso risulterà dotato di massimo e di minimo; visto e considerato che tale sottoinsieme è simmetrico rispetto allo zero, possiamo limitarci ad esaminare solo i numeri positivi.
Il minimo positivo è: \[ x_{min} = 2^{-1} \cdot 2^{-2^{h-1}} \] Il massimo positivo è: \[ x_{max} = (1 - 2^{-k}) \cdot (2^{(2^{h - 1} - 1)}) \] Ad esempio, per la codifica a \(32\) bit di Figura 16.2, il minimo positivo è: \[ x_{min} = 0 \vert 1000000 \vert 100000000000000000000000 = 2^{-1} \cdot 2^{-64} = 2^{-65} \] Il massimo positivo è: \[ x_{max} = 0 \vert 0111111 \vert 111111111111111111111111 = (1 - 2^{-24}) \cdot (2^{63}) = 2^{63} - 2^{39} \] Analizzando l'insieme dei numeri in virgola mobile così ottenuto, si può notare che, più ci si avvicina a \(x_{min}\) e più i numeri si addensano; analogamente, più ci si avvicina a \(x_{max}\) e più i numeri si diradano. In sostanza, i numeri in virgola mobile risultano distribuiti in modo non uniforme sull'asse reale.

Una ulteriore conseguenza delle considerazioni appena esposte è che per i numeri in virgola mobile non sono più valide le proprietà tipiche dei numeri reali; non risultano applicabili quindi le seguenti proprietà: \begin{align} & a + b = b + a \qquad a \cdot b = b \cdot a \qquad (commutativa) \cr & a + (b + c) = (a + b) + c \qquad a \cdot (b \cdot c) = (a \cdot b) \cdot c \qquad (associativa) \cr & a \cdot (b + c) = (a \cdot b) + (a \cdot c) \qquad (distributiva) \end{align} Può anche capitare che, effettuando addizioni, sottrazioni, moltiplicazioni e divisioni tra numeri in virgola mobile, si ottenga un risultato che non appartiene all'insieme dei numeri in virgola mobile.

Per ovviare a tutti questi inconvenienti, si ricorre a procedure che hanno lo scopo di effettuare appositi arrotondamenti; in questo modo, i vari calcoli producono sempre un risultato che ricade nell'insieme dei numeri in virgola mobile.

16.3.3 Numeri in virgola fissa

Esiste anche un sistema di codifica dei numeri reali che prevede una posizione fissa per la virgola; si parla allora di numeri in virgola fissa o fixed point numbers. In sostanza, data una codifica a \(n\) bit, si stabilisce a priori in quale bit debba trovarsi la virgola; a questo punto, tutti i bit alla sinistra della virgola contengono la parte intera, mentre tutti i bit alla sua destra contengono la parte frazionaria del numero da rappresentare. Chiaramente, si tratta di una rappresentazione dei numeri reali in forma non normalizzata.
Il vantaggio di questo sistema è dato dalla notevole semplificazione nella esecuzione di addizioni e sottrazioni in quanto i numeri risultano già incolonnati correttamente; lo svantaggio evidente è che, con \(n\) bit, si ottiene un insieme di numeri in virgola fissa enormemente meno esteso di quello dei numeri in virgola mobile.
Consideriamo, ad esempio, una codifica a \(32\) bit, con i primi \(16\) bit che contengono la parte frazionaria, i successivi \(15\) bit che contengono la parte intera e il bit più significativo che codifica il segno; in questo caso, il minimo positivo è: \[ x_{min} = 000000000000000,000000000000000{1}_2 = 2^{-16} \] mentre il massimo positivo è: \[ x_{max} = 111111111111111,111111111111111{1}_2 \lt 2^{15} = 32768 \]

16.4 Formati standard IEEE per i numeri in virgola mobile

Se proviamo a scrivere un programma che definisce una variabile del tipo:
float_var32    dd    3.625
e poi la visualizza sullo schermo attraverso la procedura writeBin32, otteniamo il seguente output:
01000000011010000000000000000000B
Come si può notare, tale output è completamente diverso da quello che avevamo ottenuto in un precedente esempio relativo proprio al numero \(x = 3,625\).
Questa differenza si spiega con il fatto che, tutti i compilatori, assemblatori e interpreti, utilizzano un metodo standard di codifica dei numeri in virgola mobile, definito dalla IEEE (Institute of Electrical and Electronics Engineers); tale metodo è molto più sofisticato di quello presentato in precedenza negli esempi che avevano uno scopo puramente didattico.

Lo standard IEEE 754 o IEEE Standard for Floating-Point Arithmetic (e successive revisioni) definisce il formato utilizzato per la rappresentazione dei numeri in virgola mobile sui computer; inoltre, si occupa della codifica di casi particolari come l'infinito positivo e negativo, il risultato di una operazione non valida (come una divisione per zero o il logaritmo di un numero negativo). Lo standard definisce anche le operazioni matematiche eseguibili sui numeri in floating point, i metodi di arrotondamento e la gestione delle eccezioni.

Sono previsti tre formati binari principali per i numeri in virgola mobile: Restano pienamente validi i concetti di bit di segno, mantissa e caratteristica che abbiamo analizzato in precedenza; vedremo però che determinati valori di tali campi vengono utilizzati per codificare casi particolari.

Consideriamo quindi una codifica binaria a \(n\) bit, con \(h\) bit per la caratteristica, \(k\) bit per la mantissa e un bit riservato al segno della mantissa; si ha quindi \(n = h + k + 1\).

Il bit più significativo, indicato con \(S\), codifica il segno della mantissa; come già sappiamo, il valore \(0\) indica segno positivo, mentre il valore \(1\) indica segno negativo.

Gli \(h\) bit che precedono \(S\) codificano la caratteristica, indicata con \(e\); per quanto visto negli esempi precedenti, dovrebbe trattarsi di un numero con segno in complemento a \(2\), modulo \(2^h\), che rappresenta l'esponente della base \(2\). Il problema che si presenta è che, con questo tipo di codifica della caratteristica, si creano notevoli complicazioni quando si devono eseguire confronti tra numeri in virgola mobile; come sappiamo, tutto ciò si traduce in una maggiore complessità dei circuiti logici che svolgono tali confronti.
Per ovviare a questo inconveniente, si ricorre ad un metodo di codifica della caratteristica detto eccesso p, con il numero p che rappresenta il cosiddetto bias; innanzi tutto, il valore \(p\) viene calcolato come segue: \[ p = (2^h / 2) - 1 = 2^{h - 1} - 1 \] Il valore \(p\) così ottenuto viene sommato alla vera caratteristica \(E\) in modo da ottenere una caratteristica sempre positiva: \[ e = E + p \] I valori \(e = 0\) e \(e = 2^h - 1\) sono riservati per gli scopi che vengono illustrati più avanti; restano a disposizione quindi \(2^h - 2\) valori di \(e\) per la codifica della vera caratteristica \(E\).
Supponiamo di avere, ad esempio, \(h = 8\); in tal caso si ha \(p = 2^7 - 1 = 127\), mentre i valori \(e = 0\) e \(e = 2^8 - 1 = 255\) sono riservati. Con \(2^8 = 256\) valori binari, possiamo rappresentare tutti i numeri interi relativi \(E\) compresi tra \(-128\) e \(+127\); quindi: \[E = -128, -127, -126, -125, \dots, -3, -2, -1, 0, +1, +2, +3, \dots, +125, +126, +127\] La caratteristica \(e\) corretta con il bias \(p = 127\) è allora: \[e = E + p = -1, 0, +1, +2, \dots, +124, +125, +126, +127, +128, +129, +130, \dots, +252, +253, +254\] Osserviamo ora che \(e = -1\) con \(8\) bit si scrive \(11111111_2\), che in base \(10\) corrisponde a \(255\); come è stato appena spiegato, tale valore è riservato, per cui non possiamo rappresentare \(E = -128\). Anche \(e = 0\) è riservato, per cui non possiamo rappresentare \(E = -127\); in definitiva, i valori di \(E\) disponibili vanno da \(-126\) a \(+127\). Sommando il bias \(127\) a \(E\) otteniamo le corrispondenti codifiche di \(e\) che vanno da \(+1\) a \(+254\), con \(E = 0\) codificato come \(+127\); la caratteristica \(e\) risulterà essere quindi sempre un numero positivo!

Grazie a questo metodo, il confronto tra numeri in virgola mobile si riduce ad un confronto tra numeri positivi. Si può notare che, se i due numeri in virgola mobile da confrontare hanno segno \(S\) diverso, ovviamente il più grande è quello con \(S = 0\) (mantissa positiva); se il segno è lo stesso, si passa al confronto delle caratteristiche. Se le caratteristiche sono diverse, ovviamente il numero più grande (in valore assoluto) è quello con la caratteristica maggiore; se le due caratteristiche sono uguali, si passa infine al confronto tra le mantisse.

La mantissa \(m\), come al solito, rappresenta le cifre dopo la virgola; a differenza però di quanto illustrato nei precedenti esempi, lo standard IEEE 754 prevede che \(m\) debba essere interpretata come \(\pm(1,...)\). Abbiamo quindi un bit nascosto che implicitamente vale sempre \(1\); in totale, per la codifica della mantissa risultano disponibili \(k + 1\) bit effettivi.
Per "forma normalizzata" in questo caso si intende il fatto che il primo bit non nullo del numero da rappresentare deve trovarsi immediatamente alla sinistra della virgola; ad esempio, normalizzare \(111,01001{1_2}\) significa convertirlo in: \[ 1,1101001{1_2} \cdot 2^2 \qquad (E = 2, m = 1101001{1_2}) \] Analogamente, normalizzare \(-0,0010010{1_2}\) significa convertirlo in: \[ -1,0010100{0_2} \cdot 2^{-3} \qquad (E = -3, m = -0010100{0_2}) \] Nel momento in cui vogliamo decodificare un numero in virgola mobile codificato secondo lo standard IEEE 754, dobbiamo prima ricavare caratteristica e mantissa in questo modo: \begin{align} E &= e - p \cr M &= 1,m \end{align} A questo punto, la decodifica del numero risulterà essere: \[ x = (-1)^S \cdot 2^E \cdot M \] Analizziamo ora i casi particolari, i quali vengono codificati assegnando determinati valori alla caratteristica e alla mantissa; nel caso più generale che abbiamo appena illustrato, si hanno i numeri in virgola mobile in forma normalizzata, con caratteristica compresa tra \(1\) e \(2^h - 2\) e mantissa maggiore o uguale a zero in valore assoluto.

Se la caratteristica e la mantissa sono entrambe nulle, si ha la codifica del numero zero; il bit di segno può assumere i due valori \(0\) e \(1\), per cui lo zero avrà le due possibili rappresentazioni \(+0\) e \(-0\).

Se la caratteristica è zero, mentre la mantissa è diversa da zero, si ha la codifica dei cosiddetti numeri denormalizzati; si tratta di numeri compresi, in valore assoluto, tra zero e il più piccolo numero normalizzato rappresentabile (estremi esclusi). Abbiamo visto che la forma normalizzata impone che la mantissa sia del tipo \(\pm(1,...)\); togliendo questo vincolo è possibile usare la mantissa stessa per rappresentare numeri ancora più piccoli di quelli normalizzati.
Per i numeri denormalizzati, convenzionalmente si pone \(E = -(2^{h - 1} - 2)\); attenzione quindi al fatto che si tratta di una convenzione, per cui non si deve calcolare \(E = e - p\) (ad esempio, per \(h = 8\) si deve porre \(E = -(2^7 - 2) = -126\) e non \(E = e - p = 0 - 127 = -127\))!
La vera mantissa per i numeri denormalizzati è data da \(M = 0,m\); il bit nascosto vale quindi \(0\) e non \(1\).
Consideriamo il caso \(h = 8\), \(k = 23\); il minimo positivo in forma normalizzata è quindi: \[ xn_{min} = 2^{-126} \cdot (1,0000000000000000000000{0_2}) = 2^{-126} \cdot 1 = 2^{-126} \] In forma denormalizzata si ha \(E = -(2^7 - 2) = -126\); il minimo positivo è quindi: \[ xd_{min} = 2^{-126} \cdot (0,0000000000000000000000{1_2}) = 2^{-126} \cdot 2^{-23} = 2^{-149} \] Il massimo positivo è: \[ xd_{max} = 2^{-126} \cdot (0,1111111111111111111111{1_2}) = 2^{-126} \cdot (1 - 2^{-23}) = 2^{-126} - 2^{-149} \] che è inferiore al minimo positivo in forma normalizzata.

Se la caratteristica vale \(255\), mentre la mantissa è zero, si ha la codifica dell'infinito; il bit di segno può valere \(0\) o \(1\) e ciò permette di rappresentare, rispettivamente, \(+\infty\) e \(-\infty\).

Se la caratteristica vale \(255\), mentre la mantissa è diversa da zero, si ha la codifica di un risultato frutto di una operazione non valida (ad esempio, \((0 / 0)\), \((0 \cdot \infty)\), \((\sqrt{-1})\), etc); si utilizza in questo caso la sigla NaN che sta per Not a Number.
Il bit di segno viene generalmente ignorato; il primo bit della mantissa, invece, indica il tipo di NaN e vale \(1\) per il quiet NaN (o qNaN) e \(0\) per il signaling NaN (o sNaN).
Un qNaN viene prodotto da una operazione non valida e si propaga tra le operazioni successive senza che venga segnalato; è compito del software quindi effettuare le necessarie verifiche.
Un sNaN viene prodotto da una operazione non valida ed è accompagnato da una apposita segnalazione; nel capitolo successivo vedremo che la FPU segnala un sNaN attraverso apposite eccezioni, le quali possono essere intercettate e gestite dai programmi.

Analizziamo ora le caratteristiche dei principali formati binari per i numeri in virgola mobile; questi e altri formati vengono illustrati più in dettaglio nel capitolo successivo.

16.4.1 Formato single precision a 32 bit

Il formato single precision a 32 bit è il più importante in quanto è l'unico considerato obbligatorio dallo standard IEEE 754; tutti gli altri formati sono considerati opzionali.
La Figura 16.3 illustra la struttura del formato in precisione singola a 32 bit. Il bit S in posizione 31 rappresenta il segno della mantissa. Gli 8 bit che precedono S rappresentano la caratteristica in eccesso \(p\); abbiamo quindi \(p = 2^7 - 1 = 127\).
I 23 bit meno significativi rappresentano la mantissa.

Per i numeri normalizzati abbiamo i seguenti estremi positivi: \begin{align} x_{min} &= 2^{-126} \cdot (1,0000000000000000000000{0_2}) = 2^{-126} \cdot 1 = 2^{-126} \cr x_{max} &= 2^{127} \cdot (1,1111111111111111111111{1_2}) = 2^{127} \cdot (1 - 2^{-23}) = 2^{127} - 2^{104} \end{align} Gli stessi estremi in base \(10\) sono: \begin{align} x_{min} &= 1,18 \cdot 10^{-38} \cr x_{max} &= 3,40 \cdot 10^{38} \end{align} La precisione è pari a 6-7 cifre significative dopo la virgola.

A questo punto siamo in grado di spiegare la codifica IEEE 754 in single precision per il numero \(x = 3,625\), mostrata in precedenza. Questo numero in binario si scrive \(x = 11,101_2\), che normalizzato diventa \(x_n = 1,1101_2 \cdot 2^1\); la mantissa a \(23\) bit è quindi \(m = 1101000000000000000000{0_2}\).
La caratteristica è \(E = 1\); in eccesso \(p\) otteniamo allora: \[ e = E + p = 1 + 127 = 128 = 1000000{0_2} \] Essendo \(S = 0\), si ha infine la codifica a \(32\) bit: \[ x = 0 \vert 10000000 \vert 11010000000000000000000 \]

16.4.2 Formato double precision a 64 bit

La Figura 16.4 illustra la struttura del formato in precisione doppia a 64 bit. Il bit S in posizione 63 rappresenta il segno della mantissa. Gli 11 bit che precedono S rappresentano la caratteristica in eccesso \(p\); abbiamo quindi \(p = 2^{10} - 1 = 1023\).
I 52 bit meno significativi rappresentano la mantissa.

Per i numeri normalizzati abbiamo i seguenti estremi positivi: \begin{align} x_{min} &= 2^{-1022} \cdot (1,00...0{0_2}) = 2^{-1022} \cdot 1 = 2^{-1022} \cr x_{max} &= 2^{1023} \cdot (1,11...1{1_2}) = 2^{1023} \cdot (1 - 2^{-52}) = 2^{1023} - 2^{971} \end{align} Gli stessi estremi in base \(10\) sono: \begin{align} x_{min} &= 2,23 \cdot 10^{-308} \cr x_{max} &= 1,79 \cdot 10^{308} \end{align} La precisione è pari a 15-16 cifre significative dopo la virgola.

16.4.3 Formato quadruple precision a 128 bit

La Figura 16.5 illustra la struttura del formato in precisione quadrupla a 128 bit. Il bit S in posizione 127 rappresenta il segno della mantissa. I 15 bit che precedono S rappresentano la caratteristica in eccesso \(p\); abbiamo quindi \(p = 2^{14} - 1 = 16383\).
I 112 bit meno significativi rappresentano la mantissa.

Per i numeri normalizzati abbiamo i seguenti estremi positivi: \begin{align} x_{min} &= 2^{-16382} \cdot (1,00...0{0_2}) = 2^{-16382} \cdot 1 = 2^{-16382} \cr x_{max} &= 2^{16383} \cdot (1,11...1{1_2}) = 2^{16383} \cdot (1 - 2^{-112}) = 2^{16383} - 2^{16271} \end{align} Gli stessi estremi in base \(10\) sono: \begin{align} x_{min} &= 3,3621 \cdot 10^{-4932} \cr x_{max} &= 1,1897 \cdot 10^{4932} \end{align} La precisione è pari a 34 cifre significative dopo la virgola.

16.4.4 Operazioni raccomandate dallo standard IEEE 754

Lo standard IEEE 754 comprende una serie di operazioni rivolte ai numeri in floating point; tali operazioni devono essere implementate dalle librerie matematiche dei linguaggi di programmazione e dai coprocessori matematici.
In particolare, devono essere forniti i principali operatori aritmetici (addizione, sottrazione, moltiplicazione e divisione), operatori di conversione tra formati, operatori di manipolazione del segno (valore assoluto, negazione, etc), operatori di comparazione, operatori di gestione dei NaN, operatori di gestione dei flags.

Lo standard IEEE 754 raccomanda anche una serie di operazioni opzionali che le librerie matematiche dei linguaggi di programmazione e i coprocessori matematici possono implementare; le principali operazioni raccomandate sono le seguenti: \begin{align} \DeclareMathOperator{\arcsinh}{arcsinh} \DeclareMathOperator{\arccosh}{arccosh} \DeclareMathOperator{\arctanh}{arctanh} & e^x, \quad 2^x, \quad 10^x \cr & e^x - 1, \quad 2^x - 1, \quad 10^x - 1 \cr & \ln x, \quad \log_2 x, \quad \log_{10} x \cr & \ln (1 + x), \quad \log_2 (1 + x), \quad \log_{10} (1 + x) \cr & \sqrt{x^2 + y^2} \cr & \sqrt{x} \cr & (1 + x)^n \cr & x^{1 \over n} \cr & x^n, \quad x^y \cr & \sin x, \quad \cos x, \quad \tan x \cr & \arcsin x, \quad \arccos x, \quad \arctan x \cr & \sin(\pi x), \quad \cos(\pi x) \cr & {{\arctan x} \over \pi} \cr & \sinh x, \quad \cosh x, \quad \tanh x \cr & \arcsinh x, \quad \arccosh x, \quad \arctanh x \end{align}

16.4.5 Metodi di arrotondamento

Lo standard IEEE 754 prevede cinque metodi di arrotondamento da applicare ai numeri in floating point. Il numero in floating point viene arrotondato verso il numero intero più vicino; ad esempio, \(+7,41327\) viene arrotondato a \(+7,0\), mentre \(-3,8972\) viene arrotondato a \(-4,0\).
Se il numero in floating point cade a metà tra due numeri interi, l'arrotondamento avviene verso il numero intero pari più vicino (si ricordi che un numero intero pari ha il bit meno significativo che vale zero); ad esempio, \(+7,5\) viene arrotondato a \(+8,0\) e anche \(+8,5\) viene arrotondato a \(+8,0\).

Si tratta del metodo di arrotondamento predefinito per i numeri in virgola mobile. Il numero in floating point viene arrotondato verso il numero intero più vicino; ad esempio, \(+15,321\) viene arrotondato a \(+15,0\), mentre \(-1,9826\) viene arrotondato a \(-2,0\).
Se il numero in floating point cade a metà tra due numeri interi, l'arrotondamento avviene verso il numero intero immediatamente maggiore per i numeri positivi e verso il numero intero immediatamente minore per i numeri negativi; ad esempio, \(+8,5\) viene arrotondato a \(+9,0\), mentre \(-6,5\) viene arrotondato a \(-7\). Il numero in floating point viene arrotondato al numero intero più vicino verso lo zero; ad esempio, \(+15,321\) viene arrotondato a \(+15,0\), mentre \(-1,9826\) viene arrotondato a \(-1,0\).

Come si può notare, questo metodo consiste semplicemente nel troncare la parte frazionaria del numero in floating point. Il numero in floating point viene arrotondato al numero intero più vicino verso \(+\infty\); ad esempio, \(+15,321\) viene arrotondato a \(+16,0\), mentre \(-1,9826\) viene arrotondato a \(-1,0\). Il numero in floating point viene arrotondato al numero intero più vicino verso \(-\infty\); ad esempio, \(+15,321\) viene arrotondato a \(+15,0\), mentre \(-1,9826\) viene arrotondato a \(-2,0\).

16.4.6 Gestione delle eccezioni

Lo standard IEEE 754 definisce cinque eccezioni per il risultato di una operazione tra numeri in floating point; ciascuna eccezione produce un valore predefinito. Questa eccezione viene generata quando una operazione produce un risultato non definito matematicamente; ad esempio, \(\sqrt{-1}\) produce un risultato che non appartiene ad \(\Bbb R\).

Il valore predefinito restituito da questa eccezione è un qNaN. Questa eccezione viene generata quando una operazione produce un risultato infinitamente grande; ad esempio, \(3 / 0 = +\infty\), mentre \(\log_2 {0^+} = -\infty\).

Il valore predefinito restituito da questa eccezione è \(\pm\infty\). Questa eccezione viene generata quando una operazione produce un risultato più grande (in valore assoluto) del massimo rappresentabile attraverso il formato che si sta utilizzando.

Il valore predefinito restituito da questa eccezione dipende dal metodo di arrotondamento in uso.
Se il risultato è positivo, il valore predefinito è \(+\infty\) per to nearest, il più grande numero positivo rappresentabile per toward \(-\infty\), \(+\infty\) per toward \(+\infty\) e il più grande numero positivo rappresentabile per toward zero.
Se il risultato è negativo, il valore predefinito è \(-\infty\) per to nearest, \(-\infty\) per toward \(-\infty\), il più grande numero negativo rappresentabile per toward \(+\infty\) e il più grande numero negativo rappresentabile per toward zero. Questa eccezione viene generata quando una operazione produce un risultato più piccolo (in valore assoluto) del minimo rappresentabile attraverso il formato che si sta utilizzando.

Il valore predefinito restituito da questa eccezione è un numero denormalizzato; se tale numero non è rappresentabile in modo esatto, si procede come descritto qui sotto per l'eccezione Inexact. Questa eccezione viene generata quando una operazione produce un risultato che non può essere rappresentato in modo esatto attraverso il formato che si sta utilizzando.

Il valore predefinito restituito da questa eccezione è il risultato arrotondato in base al metodo di arrotondamento in uso.

Bibliografia

754-2008 - IEEE Standard for Floating-Point Arithmetic disponibile sul sito ufficiale della IEEE.org (è richiesta la registrazione)

In alternativa, si può consultare il documento sui siti web di varie università; ad esempio:
754-2008 - IEEE Standard for Floating-Point Arithmetic Université de La Réunion
754-2008 - IEEE Standard for Floating-Point Arithmetic Universidade Federal de Campina Grande