Articolo originale: What is a Floating-Point Arithmetic Problem?
Ti è mai capitato di lavorare con numeri come 1/3, il cui risultato è 0.33333... continuando all'infinito? Noi umani di norma arrotondiamo numeri del genere, ma ti sei mai chiesto come vengono gestiti dai computer?
In questo articolo esploreremo il modo in cui i computer gestiscono i numeri decimali illimitati, includendo il concetto degli errori di precisione. Esamineremo il problema aritmetico della virgola mobile, una problematica universale che riguarda vari linguaggi di programmazione. Ci focalizzeremo in particolare su come JavaScript affronta questo problema.
Inoltre, impareremo come funzionano le operazioni binarie dietro le quinte, il limite oltre il quale JavaScript tronca i numeri, basandosi sullo standard IEEE 754, e introdurremo BigInt
come soluzione per gestire in maniera accurata grandi numeri senza perdere precisione.
Innanzitutto prendiamo in considerazione un esempio. Riesci ad indovinare il risultato di questa operazione?
console.log(0.1 + 0.2);
Potresti aver pensato che la risposta fosse 0.3, vero? Ma in realtà il risultato è:
Output: 0.30000000000000004
Ti starai chiedendo perché ciò accada. Perché ci sono così tanti zero in più, e perché l'ultima cifra è 4?
La risposta è semplice: i numeri 0.1 e 0.2 non possono essere rappresentati in maniera precisa in JavaScript (ovvero "esattamente" o "accuratamente")
Sembrerebbe facile, no? Ma la spiegazione è un po' più complicata.
Quindi cosa ne pensi? È un bug o una funzionalità?
Be', non è un bug. È un problema di base, che ha a che fare con il metodo in cui i computer gestiscono i numeri, in particolare i numeri a virgola mobile.
Perché succede questa cosa?
Capiamolo attraverso la matematica di base.
La frazione 1/3, è rappresentata in decimali da 0.33333... dove il 3 si ripete all'infinito. Non possiamo scriverlo in maniera esatta, quindi lo approssimiamo come 0.333 o 0.333333 per risparmiare tempo e spazio.
In maniera simile, con un computer, dobbiamo comunque approssimare, perché 1/3 o 0.3333... sarebbe un numero troppo grande e occuperebbe un spazio infinito (che non abbiamo).
Ciò porta a quello che noi chiamiamo il problema aritmetico della virgola mobile.
Il problema aritmetico della virgola mobile
In parole povere, i numeri a virgola mobile sono numeri che non possono essere scritti in maniera esatta, e che quindi vengono approssimati. In un computer questo tipo di approssimazione può portare a piccoli errori di precisione, che chiamiamo il problema aritmetico della virgola mobile.
Spiegazione binaria
Adesso che ci siamo occupati di questa semplice spiegazione, comprendiamo questo concetto anche dal punto di vista binario. JavaScript gestisce tutto in codice binario dietro le quinte.
Binario è un sistema di numeri che utilizza solo due cifre: 0 e 1.
Perché 0.1 e 0.2 non possono essere rappresentati accuratamente nel sistema binario?
Il problema principale è che non tutti i numeri decimali possono essere perfettamente rappresentati come frazioni binarie.
Prendiamo 0.1 come esempio: quando si prova a rappresentare 0.1 in binario, si scopre che non può essere espresso come una frazione binaria finita, invece diventa una frazione continua, proprio come 1/3 in numeri decimali diventa 0.333..., ripetuto all'infinito.
In binario, 0.1 diventa:
0.0001100110011001100110011001100110011... (si ripete all'infinito)
Dato che i computer hanno una memoria limitata, non possono memorizzare questa sequenza infinita in maniera esatta, ma la devono troncare ad un certo punto, e ciò crea un piccolo errore di arrotondamento. Questo è il motivo per cui 0.1 in binario è solo una approssimazione dell'effettivo numero.
Come 0.1, 0.2 non può essere rappresentato accuratamente nel sistema binario. Diventa invece:
0.00110011001100110011001100110011... (si ripete all'infinito)
Di nuovo il computer tronca questa sequenza binaria infinita, causando un piccolo errore di rappresentazione.
Quindi cosa succede quando addizioniamo 0.1 + 0.2? Quando si addizionano 0.1 + 0.2 in JavaScript, le approssimazioni binarie per 0.1 e 0.2 sono sommate. Ma, dato che entrambi i valori sono solo approssimazioni, anche il risultato è una approssimazione.
Invece di ottenere esattamente 0.3, otterremo:
console.log(0.1 + 0.2); // Output: 0.30000000000000004
Questo minimo errore si verifica perché né 0.1 né 0.2 possono essere rappresentati accuratamente nel sistema binario, quindi il risultato finale ha un piccolo errore di arrotondamento.
Come tronca i numeri JavaScript?
Adesso sorge una domanda: come fa JavaScript a sapere quando deve troncare il valore (troncamento significa abbreviare un numero rimuovendo le cifre in più dopo un certo punto)?
C'è un limite massimo ed un limite minimo per farlo.
Per gestire questa operazione nel mondo dei computer, abbiamo uno standard che definisce come i numeri a virgola mobile vengono memorizzati e calcolati.
Standard IEEE 754
JavaScript utilizza lo standard IEEE 754 per gestire le operazioni aritmetiche con i numeri a virgola mobile.
Lo standard stabilisce dei limiti di numeri interi sicuri per il tipo Number
in JavaScript, che evitano la perdita di precisione:
- Massimo Numero Intero Sicuro: 2^53 - 1 o 9007199254740991
- Minimo Numero Intero Sicuro: -(2^53 - 1) o -9007199254740991
Oltre questi limiti JavaScript non può rappresentare accuratamente i numeri interi, dato il modo in cui funziona l'aritmetica dei numeri a virgola mobile.
Per questa ragione, JavaScript ci fornisce due costanti per rappresentare questi limiti:
Number.MAX_SAFE_INTEGER
Number.MIN_SAFE_INTEGER
Nel caso avessi bisogno di un numero più grande?
Se hai bisogno di lavorare con numeri più grandi del massimo numero intero sicuro (come quelli utilizzati nella crittografia o nelle finanze), JavaScript ha una soluzione: BigInt.
BigInt
BigInt
è un oggetto integrato che ti permette di lavorare con numeri interi che vanno oltre il limite degli interi sicuri. Ti permette di rappresentare numeri più grandi di 9007199254740991 senza preoccuparti degli errori di precisione!
Per utilizzare BigInt
bisogna semplicemente aggiungere una n
alla fine di un numero intero:
const bigNumber = 1234567890123456789012345678901234567890n;
In alternativa, puoi utilizzare il costruttore BigInt
:
const bigNumber = BigInt("1234567890123456789012345678901234567890");
Calcoli con BigInt
Si possono svolgere operazioni aritmetiche utilizzando BigInt
, come addizione, sottrazione, moltiplicazione, e anche elevamento a potenza. Tuttavia, c'è un problema: non possiamo fare operazioni aritmetiche mescolando BigInt
con il tipo Number
, non senza prima fare delle conversioni esplicite tra un tipo e l'altro.
Per esempio, questo non funzionerà:
let result = bigNumber + 5; // Error: cannot mix BigInt and other types
C'è bisogno prima di convertire Number
in BigInt
:
let result = bigNumber + BigInt(5); // Adesso funziona!
Quando utilizzare BigInt?
BigInt
è particolarmente utile in aree in cui è richiesta precisione, come ad esempio:
- Algoritmi crittografici
- Gestire grandi set di dati
- Calcoli finanziari che richiedono precisione
Riassumendo
- Il limite intero sicuro in JavaScript garantisce una rappresentazione accurata dei numeri tra -(2^53 - 1) e 2^53 - 1.
- Gli errori di precisione si verificano a causa dell'aritmetica dei numeri a virgola mobile, quando si lavora con particolari numeri (come 0.1 + 0.2).
- Se hai bisogno di utilizzare numeri oltre del limite sicuro,
BigInt
è tuo amico. Ma ricorda che per combinareBigInt
con il tipoNumber
bisogna prima fare delle conversioni esplicite.