Premessa
Dei vari formati in virgola mobile ho già parlato ampiamente in un vecchio articolo su questa rubrica (al quale farò spesso riferimento e che consiglio di leggere prima di addentrarsi in questa nuova analisi); in quella occasione proponevo anche dei nuovi standard non esistenti. Qui, prendendo le mosse da quel lavoro, propongo un ulteriore standard innovativo e anche un pò provocatorio, sotto diversi punti di vista...
L'idea che qui presento è il frutto di una discussione che ebbi ben 28 anni fa, quando ero uno studente di Fisica al termine del corso di laurea. Un professore che lavorava al progetto APE-100 per costruire un supercomputer dedicato a studi di cromo-dinamica quantistica mi disse che, per le esigenze della ricerca, la precisione "singola" (32 bit) era insufficiente mentre quella "double" (64 bit) risultava sovrabbondante.
In effetti, la precisione singola garantisce solo 7 cifre decimali significative mentre quella doppia arriva quasi a 16. Eppure, nella stragrande maggioranza dei calcoli, si considerano al massimo 11 cifre significative e questo, del resto, è il livello di precisione raggiunto nelle migliori misure di grandezze e di costanti fisiche fondamentali(1).
Ecco allora l'idea di un formato compatto e ottimizzato, con caratteristiche innovative che lo rendano, per certi versi, migliore rispetto a quelli attuali.
Che significa "48-i"?
Volendo creare un formato "floating" con una lunghezza intermedia tra 32 e 64 bit, la scelta più ovvia (e anche in fondo quella più azzeccata) ricade su 48 bit (6 bytes). Nel precedente articolo, in verità, avevo ignorato questa soluzione in favore di un frazionamento tra due formati non-standard da 40 e 52 bit. Ripensandoci, però, non era una scelta molto sensata: da un lato, il primo dei due formati è comunque poco preciso e quindi di dubbia utilità(2); invece il secondo ha lo svantaggio di non essere un multiplo di 8 bit (1 Byte) e poi comincia ad essere un pò troppo vicino al consolidato formato 64 bit per risultare davvero competitivo.
Nei paragrafi seguenti propongo quindi diverse possibili implementazioni di un formato a 48 bit che soddisfi i requisiti di precisione e di esponente prima menzionati; siccome molti potrebbero obiettare che esiste già un formato diffuso a 64 bit che soddisfa abbondantemente quei requisiti, ho cercato di migliorare e potenziare in vari modi questo formato "48i" rispetto ai tradizionali "Floating Point Format" (FPF) e infatti la "i" finale sta proprio per "improved". Il formato di base è quello illustrato di seguito, con 1 bit per il segno, 9 bit di esponente e 38 di mantissa o "significando", ma le implementazioni descritte di seguito lo differenziano dagli altri formati FPF canonici,
Le implementazioni:
"Opzione 0"
Questa prima opzione è un po' provocatoria perchè si discosta nettamente dal classico formato in virgola mobile (FPF) per creare uno standard che rispetti i criteri di massima semplicità e simmetria, andando in direzione contraria rispetto alle mie proposte precedenti.
Partendo comunque dal classico formato FPF, fermo restando che quando l'esponente assume il valore più basso esso indica un numero denormalizzato e cioè con virgola fissa e precisione decrescente, facciamo la stessa cosa anche per l'esponente più alto. In altre parole, una volta che l'esponente di 9 bit è “saturo”, esso comincia ad allungarsi a scapito del significando che perde progressivamente bit riducendo la precisione fino a zero. Questa “magia” è piuttosto semplice da realizzare: è sufficiente introdurre, alla fine di questo esponente di lunghezza variabile composto da una sequenza di “1”, un bit nullo che segnali il passaggio dall'esponente al significando(3). Se ci si pensa, è qualcosa di analogo a quanto succede anche nei numeri piccoli denormalizzati, dove stavolta è l'apparizione di un “1” che decreta, nel campo del significando, la prima cifra binaria significativa e quindi la precisione e l'ordine di grandezza del valore.
Dato che ci serve comunque un numero dispari di combinazioni per il campo “exp” (altrimenti la scala corrispondente non sarebbe simmetrica rispetto al valore centrale exp=”100000000” in binario, che corrisponde a un esponente nullo come valore decodificato), il valore di base (sequenza di 9 zeri) lo riserviamo comunque ai NaN (Not a Number), tra i quali figurano anche ±∞; questo è l'esatto contrario di quanto si fa normalmente ma è solo una scelta per rendere lo schema più intuitivo.
Invece di perderci in ulteriori chiacchiere, ecco uno schema che dovrebbe rendere chiaro il meccanismo, con un campionario di numeri binari decodificati (per i numeri consecutivi, l'ultima colonna mostra la variazione relativa; il bit di segno non è riportato):
Naturalmente,anche se questa opzione è "elegante" e riesce a codificare numeri che sono fino a 11 ordini di grandezza più grandi di quanto accadrebbe con il formato classico, lo fa con precisione decrescente; per il resto mantiene tutte le limitazioni del FPF tradizionale, come l'incapacità di gestire numeri razionali esatti o di manipolare valori realmente molto grandi e molto piccoli. Inoltre, si introducono insidiosi errori “invisibili” di arrotondamento anche all'astremo superiore, come già succede con numeri piccoli de-normalizzati (si vedano le considerazioni fatte alla fine della sezione “Opzione 3”).
Opzione 1
Adottando rigidamente il formato degli standard FPF già esistenti, i numeri floating a 48 bit non denormalizzati cadono nell'intervallo:
range (254) = 3,45·10-77 ÷ 1,16·1077
La precisione oscilla, in questo intervallo, tra 11,44 e 11,73 cifre decimali; detto in modo più rigoroso, l'errore relativo di arrotondamento sarà sempre ≤ 10-11,44 ≈ 3,6·10-12. Naturalmente, i numeri denormalizzati arriveranno a circa 1·10-88 con precisione decrescente. Tuttavia, la “i” non avrebbe senso se non ci fosse un minimo miglioramento rispetto a questo standard; una possibile miglioria è il ricorso a un "bias" diverso e asimmetrico per il calcolo dell'esponente, in modo da privilegiare numeri grandi dato che, a mio parere, sono quelli sui quali si sente maggiormente la limitazione. Ad esempio, si potrebbe adottare un bias pari a -235 anzichè -254 in modo che il range (considerando anche i numeri denormalizzati) sia abbastanza simmetrico (da 6·10-83 a circa 6·1082), mentre per il range normalizzato si avrebbe:
range (235) = 1,81·10-71 ÷ 6,07·1082
Una soluzione più estrema è quella di spingere ancora più in alto il bias, in maniera da avere un massimo vicino a 10100:
range (178) = 2,61·10-54 ÷ 8,74·1099
In questo caso, il minimo denormalizzato si aggira su 1·10-65. Un'altra possibilità è quella di utilizzare un formato floating “decimale”, che ha il vantaggio di rendere la conversione in numeri base 10 più rigorosa e offre comunque un range leggermente più esteso.
Opzione 2
Partendo dal formato “standard” sopra descritto, si apportano le variazioni già descritte nel precedente articolo. In pratica, si tratta di cambiare solo il significato dei valori NaN che appaiono, lo ricordiamo, quando il campo esponente assume il valore più alto; si tratta di quasi 550 miliardi di combinazioni diverse (precisamente 549755813886, escludendo i valori ±∞ già presenti).
Rispetto alle proposte fatte nell'articolo precedente, tuttavia, è opportuno eliminare l'opzione di rappresentare i numeri “grandissimi” (ovvero ad esponente generalizzato), prima annidata all'interno dei numeri “molto grandi” ad esponente variabile, quello che avevo definito “Superfloat”. Questo perchè si tratta di una forma numerica estrema per rappresentare numeri difficili da manipolare, di scarsa utilità pratica e che peraltro risultano estremamente “rarefatti”.
In pratica, quando il campo “esponente” vale 511 (ovvero 111111111 in binario), i primi due bit del significando (gli “option bits”) indicano le seguenti alternative su come interpretare i successivi 36 bit:
Option Bit |
Significato dei successivi 36 bit (dominio) |
“00” |
NaN (137438953472 combinazioni con il segno, inclusi ±∞) |
“01” |
numero razionale, codificato in formato “floating slash” (con FSP=5 bit) |
“10” |
“variable exponent” per numeri grandi (con 4 bit di “field position”) |
“11” |
numero complesso (15x2 bit + 7 bit di esponente comune) |
Tabella 2: domini proposti all'interno dei NaN tradizionali (Opzione 2)
Prima di iniziare a descrivere i diversi domini, va detto che, contrariamente ai formati tradizionali FPF, qui non viene rispettata la regola per cui è sempre possibile ordinare correttamente un insieme di numeri anche trattandoli come fossero interi; questo è inevitabile a causa della presenza dei numeri razionali esatti che assumono valori coperti anche dal formato FPF standard. Tutto ciò potrebbe far pensare a una ridondanza poiché, in fondo, molti valori esatti vengono duplicati nei due formati (si pensi ad esempio a tutti i numeri razionali nella forma n/2m); tuttavia, anche valori “floating” apparentemente esatti potrebbero essere il risultato approssimato di una serie di calcoli fatti nel formato FPF, perciò è bene tenere separati i risultati poiché hanno significati concettualmente diversi. Questo stesso ragionamento va applicato anche ai due formati riservati a numeri grandi e grandissimi e, di conseguenza, ho deciso di lasciare zone di sovrapposizione tra essi anche se questo riduce l'efficienza e quindi il range di codifica; al contrario, nel precedente articolo, non avevo considerato le implicazioni del mescolare formati differenti con diverse precisioni. In altre parole, è bene che il computer tenga traccia dei passaggi fatti e della qualità dei risultati e quindi, a meno di non dedicare internamente altri bit “nascosti” allo scopo (in aggiunta ai 48 ufficiali), è opportuno tollerare ridondanze che tali non sono!
Cominciamo con l'opzione dei numeri razionali, tramite l'uso della tecnica “floating slash” descritta in precedenza. Avendo ora a disposizione solo 36 bit (più il segno), i primi 5 vengono dedicati all'indicatore di posizione della linea di frazione (“slash position field” o SPF) e i rimanenti 31 per esprimere numeratore N e denominatore D, cioè la frazione vera e propria; stavolta la linea di frazione non cade fuori dal significando perchè il valore dello SPF oscilla tra 0 e 31. Quando esso vale 0, lo slash si trova all'estrema sinistra del significando, prima del bit più significativo; in questo caso, il numeratore è implicito e vale 1 (esattamente come il “hidden bit” del significando nei floating non denormalizzati); di conseguenza, ci ritroviamo di fronte al reciproco di un intero di 31 bit (il denominatore); perciò, il numero razionale più piccolo rappresentato dal sistema sarà 1/231=1/2147483646. Quando lo slash si sposta di una posizione a destra (SPF=1), non ha senso che il numeratore possa assumere di nuovo il valore 1, perchè finiremmo per ripetere le stesse frazioni di prima (in realtà solo la metà di esse, dato che adesso il massimo denominatore si è dimezzato); perciò, per vitare lo spreco di combinazioni, è necessario che i due possibili valori di N siano 2 e 3; analogamente, allo step successivo (SPF=2), N=4,5,6 o 7 e, più in generale, avremo Nmin=2SPF e Nmax=2SPF+1-1 (quest'ultimo, tra l'altro, è anche il numero intero più grande codificato, quando il denominatore vale 1). Arrivati a SPF=31, avremo che il denominatore scompare (di fatto deve essere un implicito 1), mentre il numeratore varia tra 2147483646 e 4294967295; mettendo insieme tutti i risultati, abbiamo realizzato il piccolo “miracolo” di codificare, con 31 bit di significando, anche tutti i possibili valori di un numero intero a 32 bit (escluso lo zero che però è già presente come “zero esatto” tra i numeri denormalizzati)(4). Questo è un enorme vantaggio poiché, in pratica, significa che il sistema standard “integer 32” è di fatto contenuto in questo FPF-48i/floating-slash, il quale sembra ottimizzato per questo scopo! A questo punto, adottando altri accorgimenti atti a ridurre le ridondanze (non abbiamo fatto nulla per evitare la ripetizione di una frazione semplificabile, in cui N e D sono moltiplicati per la stessa quantità), si riuscirebbero ad eliminare molte altre combinazioni e questo probabilmente consentirebbe di portare a 232 il valore massimo del denominatore per SPF=0, codificando anche tutti i reciproci di un “integer 32”; tuttavia, la codifica delle frazioni risulterebbe ancora più complessa e forse non ne varrebbe la pena(5) .
Peraltro, si potrebbe utilizzare questo criterio anche per esprimere numeri molto piccoli ma, in tal caso, non ha più senso utilizzare i numeri denormalizzati. Perciò, la trattazione qui illustrata per i numeri molto grandi (quando il campo “esponente” principale vale 111111111) si può applicare allo stesso modo anche al caso opposto (esponente=000000000) per rappresentare numeri piccolissimi con esponente negativo di lunghezza variabile. Analizzeremo meglio questa opzione a fine articolo.
Passiamo ora alla codifica con esponente variabile, riservata ai numeri molto grandi (con esponente positivo), ma con precisione ridotta. La filosofia è simile a quella del “floating slash”, solo che stavolta a variare non è più la posizione della linea di frazione ma la lunghezza del campo dedicato all'esponente. Se si dedicano i primi 4 bit al campo “exponent lenght” λ (con un “bias” pari a 9), allora l'esponente occuperà i successivi λ bit (da 9 a 24), mentre alla mantissa vengono dedicati i rimanenti 32-λ bit (da 23 a 8). Si noti che, non essendo necessario riservare un valore dell'esponente ai NaN (comunque già codificati), al crescere di λ il valore binario dell'esponente andrà da +256 a +8388608 e questo corrisponderà a valori decimali approssimativamente compresi tra 154 e 5,05 milioni; nel contempo, la precisione numerica passerà da 6,9 a 2,4 cifre decimali (nel caso peggiore, usando l'hidden bit). Il numero più grande codificato (quando λ = 24 e il significando è una sequenza di 8 “uno”) è 1,8·105050445. La struttura del formato è quella sottostante, dove la doppia freccia rossa indica l'intervallo entro cui può oscillare la divisione tra il nuovo campo “exponent” e il significando:
Di seguito, le perestazioni dei due standard FPF48 classico e variable exponent, in funzione dell'esponente decimale del numero rappresentato; nel primo grafico, il dettaglio per esponenti relativamente piccoli e anche negativi fa vedere il rapido degrado sulla sinistra dovuto ai numeri denormalizzato (la cui eliminazione in favore di un “variable exponent” negativo renderebbe il grafico simmetrico); sulla destra dello stesso grafico, invece, si nota il gradone dovuto al passaggio tra i due sistemi con una drastica riduzione di precisione numerica. Nel grafico sulla destra si è dovuto ricorrere a una scala logaritmica per l'esponente (!) per mostrare l'enorme intervallo di ordini di grandezza abbracciati e si vedono i gradini successivi, dovuti all'aumento di λ.
Anche qui c'è un problema apparente di ridondanza: in aggiunta alla già citata sovrapposizione con il sistema FPF48 normale, i valori “variable exponent” si sovrappongono anche tra di loro poiché, ogni volta che λ cambia, il significando riparte dal valore più basso (1,00... considerando l'hidden bit). Tuttavia, non si tratta di valori concettualmente identici poiché, di nuovo, ognuno di essi riflette un livello di precisione numerica diverso; di fatto, è come se esistessero 16 formati diversi al variare di λ ed è meglio che essi rimangano separati a testimoniare i differenti gradi di approssimazione dei numeri rappresentati. Anche qui mi sono discostato dall'articolo precedente che, per evitare ridondanze, calcolava l'esponente variabile in modo più complesso e raggiungeva per questo valori più elevati, mescolando però le carte!
Arriviamo infine ai numeri complessi; la scarsa disponibilità di bit induce all'uso di un esponente comune di 7 bit e 15 bit per ciascuna delle due componenti reale e immaginaria, segno compreso; la somma sarebbe 37 e non 36 ma, per la parte reale, si può fare uso del bit “sign” iniziale, altrimenti non sfruttato. La precisione numerica risulta pari a 4 cifre decimali significative (con un margine di 0,5 decimali) e l'esponente decimale è compreso nell'intervallo ±19; entrambe queste caratteristiche sono decisamente limitanti e questo renderebbe poco appetibile l'uso di tale opzione.
Opzione 3
L'eventualità di eliminare dal formato i numeri complessi può essere legata all'idea di eliminare anche i numeri denormalizzati, sostituendoli con dei “variable exponent” molto piccoli; una simile soluzione, accennata nella sezione precedente a proposito dei numeri razionali reciproci, è uno scostamento importante dal formato FPF canonico ma risulta appetibile sia perchè simmetrica e quindi “elegante”, sia perchè permetterebbe di risparmiare un bit mettendolo a disposizione nei formati razionali e “variable exponent”. In effetti, come mostrato nella tabella seguente, per ciascuno dei valori estremi dell'esponente si può utilizzare un solo “option bit”, che quindi di fatto risulta duplicato generando quattro scelte alternative:
Esponente |
Option Bit |
Significato dei successivi 37 bit (dominio) |
'000000000' |
“0” |
NaN (274877906944 combinazioni con il segno, inclusi ±∞) |
'000000000' |
“1” |
Esponente variabile per numeri molto piccoli (4 bit di “field position”) |
'111111111' |
“0” |
Numero razionale codificato in formato “floating slash” (5 bit di SPF) |
'111111111' |
“1” |
Esponente variabile per numeri molto grandi (4 bit di “field position”) |
Tabella 3: nuovi domini proposti all'interno dei NaN tradizionali (Opzione 3)
E' chiaro che la presenza di un bit ulteriore non porta cambiamenti radicali: il numero di codici NaN è raddoppiato, i numeri razionali adesso possono andare da 2-32 a 233 e la precisione dei “variable exponent” è cresciuta di 0,3 cifre decimali. Tuttavia, adesso, il numero più piccolo rappresentabile non è 1·10-88 (il più piccolo de-normalizzato, con precisione pressoché nulla) ma è dell'ordine di 10-5050445 !
Peraltro, questa simmetria non soddisfa semplicemente l'estetica ampliando enormemente il numero di ordini di grandezza abbracciato, ma ha implicazioni sostanziali: infatti, l'uso di numeri de-normalizzati, quindi con poche cifre significative, può gravemente degradare la precisione finale di calcoli in cui dovessero essere usati, senza che l'utente ne sia informato! Al contrario del sistema qui presentato, infatti, i vari formati FPF classici non conservano l'informazione relativa ad eventuali drastici arrotondamenti derivanti da questa eventualità. Questo a mio parere è un difetto piuttosto grave, al quale si è cercato di ovviare in parte utilizzando, internamente, un numero maggiore di bit (si pensi alla “extended” precisioni di 80/96 bit utilizzato da diversi processori), con ulteriore aggravio di memoria utilizzata. Invece usando un sistema a esponente variabile, nell'effettuare calcoli i numeri risultanti ereditano la categoria di precisione (cioè il valore di λ) dell'operando con approssimazione peggiore e questo è sicuramente il modo più corretto di operare, almeno quando si effettuano moltiplicazioni/divisioni.
I grafici relativi a questa opzione differiscono di poco dai precedenti e non vengono qui riproposti per risparmiare spazio, tranne quello sulle performances riportato in apertura su entrambi i fronti di numeri molto grandi e molto piccoli.
Conclusioni
Il sistema FPF 48i, specialmente nella opzione 3 “simmetrica”, offre indubbi vantaggi in termini di compattezza, flessibilità e anche rigorosità rispetto agli attuali standard. L'enorme dinamica dell'esponente, unita alla possibilità di manipolare numeri razionali esatti e, sul versante opposto, di tenere traccia dell'ordine di arrotondamento utilizzato nei calcoli, ha dei vantaggi concreti da non sottovalutare.
Nell'estate 2020 ho scritto un altro articolo in cui presento una nuova codifica COVP (“Compact Optimized Variable Precision”) che potrebbe rendere davvero competitivo l'uso di numeri floating a 48 bit.
Naturalmente, non mi illudo che questo possa diventare uno standard reale poiché ormai tutti i sistemi di calcolo sono consolidati attorno agli standard esistenti e, in particolare, i due FPF a singola e doppia precisione; esso andava semmai proposto e adottato alcuni decenni fa, quando si doveva effettuare la transizione dai vecchi sistemi a 32 bit verso standard più precisi e le alternative non erano ovvie. Purtroppo, come spesso accade anche in campo informatico, efficienza, fantasia e lungimiranza hanno avuto la peggio. Tuttavia, credo che questo articolo possa offrire interessanti spunti di riflessione, ispirando magari future evoluzioni.
Note:
- peraltro, è improbabile che tale precisione possa migliorare negli anni a venire dato che si sono ormai raggiunti i limiti stessi della definizione di misura e il valore per molte di quelle costanti è ora "congelato" nel Sistema Internazionale!
- in realtà un formato FPF a 40 bit, l'MBF, era stato introdotto da Microsoft negli anni '70 ma era diverso da quello da me proposto perchè continuava ad utilizzare solo 8 bit per l'esponente.
- Al limite, l'esponente si allunga fino a consumare tutti i 38 bit disponibili e in tal caso lo zero di separazione è sottinteso, mentre il valore “nascosto” del significando è 1.
- Naturalmente non è un vero miracolo poichè il trucco è quello di avere usato in tutto 36 bit, generando quindi un totale di codifiche 16 volte più ampio di quello dei soli “integer 32”.
- Una possibile alternativa al metodo “algoritmico” per codificare/decodifica re i numeri razionali sarebbe quella di tabulare tutte le frazioni non semplificabili, usando una sorta di “look-up table” precalcolata cui fare riferimento. Tuttavia, anche se a quel punto l'efficienza sarebbe massima, questo metodo richiederebbe l'uso di svariati GB di memoria dedicata!