Precisione in virgola mobile: perché 0.1 + 0.2 ≠ 0.3
0.1 + 0.2 restituisce 0.30000000000000004 perché né 0.1 né 0.2 esistono dentro un numero binario in virgola mobile. Un double può contenere solo valori della forma m × 2ⁿ, e un decimo in base 2 è una frazione periodica infinita, quindi l’hardware si tiene il vicino rappresentabile più prossimo. Il double dietro 0.1 è esattamente 0.1000000000000000055511151231257827021181583404541015625. Quello dietro 0.2 è 0.200000000000000011102230246251565404236316680908203125. Somma questi due valori memorizzati e la somma esatta cade tra due double rappresentabili. IEEE 754 arrotonda a quello più vicino, che sta un capello sopra 0.3.
Questa è tutta la risposta. La precisione in virgola mobile è finita e le frazioni decimali entrano di rado nella griglia binaria; poi ogni operazione arrotonda di nuovo il proprio risultato.
Non è una stranezza di JavaScript né un difetto della CPU. Lo stesso risultato compare in Python, Java, C, Go, Rust, Swift e nelle colonne FLOAT di SQL, perché girano tutti su IEEE 754. Incolla un valore qualsiasi nel convertitore IEEE 754 per numeri in virgola mobile gratuito per vedere i bit esatti e il valore memorizzato esatto, cifra per cifra.
Restano da vedere il perché dell’errore, il motivo per cui il tuo linguaggio a volte lo nasconde, come confrontare i float senza == e quale tipo numerico scegliere quando la precisione costa denaro.
La precisione in virgola mobile in 60 secondi
Tre fatti spiegano quasi ogni sorpresa:
- La virgola mobile binaria può rappresentare solo numeri della forma m × 2ⁿ, cioè una somma di potenze di due.
- Le frazioni decimali come
0.1,0.2e0.3non hanno quella forma, quindi vengono arrotondate all’ingresso. - Ogni operazione aritmetica arrotonda di nuovo il proprio risultato al valore rappresentabile più vicino.
| Decimale | Rappresentabile esattamente? | Perché |
|---|---|---|
| 0.5 | Sì | 2⁻¹ |
| 0.25 | Sì | 2⁻² |
| 0.75 | Sì | 2⁻¹ + 2⁻² |
| 2.5 | Sì | 2 + 2⁻¹ |
| 100 | Sì | Qualsiasi intero sotto 2⁵³ |
| 0.1 | No | 1/10 — il denominatore ha un fattore 5 |
| 0.2 | No | 1/5 — stesso problema |
| 0.3 | No | 3/10 — stesso problema |
Regola pratica: riduci la frazione e guarda il denominatore. Se è una pura potenza di due, il valore è esatto; qualsiasi fattore 5 che sopravvive significa che viene memorizzato in modo approssimato.
Perché 0.1 non può essere memorizzato esattamente
I bit dopo la virgola binaria portano i pesi 1/2, 1/4, 1/8, 1/16, 1/32 e così via. Prova a comporre 0.1 con quelli e non ci arrivi mai. Ottieni 1/16 = 0.0625, più 1/32 = 0.09375, più 1/256 = 0.09765625: ti avvicini a ogni passo senza mai arrivarci. In binario, un decimo è 0.0001100110011… con 0011 che si ripete all’infinito.
Il sistema decimale ha lo stesso problema con frazioni diverse. Scrivere 1/3 in base 10 dà 0.333… e nessuna stringa finita di cifre lo centra. Nessuno lo chiama un bug del decimale. La base 2 traccia semplicemente il confine altrove, e 1/10 capita che cada dal lato sbagliato. La documentazione Python, Floating Point Arithmetic: Issues and Limitations, ripercorre la stessa espansione se vuoi la versione lunga.
Il binario è solo la base 2, e la stessa aritmetica posizionale muove esadecimale e ottale. Il convertitore binario, esadecimale, decimale e ottale mostra come un valore si sposta tra le basi, e la nostra guida alla conversione tra basi numeriche copre in profondità il lato degli interi. Le frazioni sono il punto in cui la base 2 inizia a far male.
Cosa memorizza davvero un double
Un double a 64 bit si divide in tre campi: 1 bit di segno, 11 bit di esponente e 52 bit di mantissa. La mantissa porta un 1 iniziale implicito che non viene mai memorizzato, quindi ottieni 53 bit di significando, cioè circa 15.95 cifre decimali di precisione in virgola mobile. L’esponente viene memorizzato con un bias di 1023.
Chiedi a un linguaggio qualsiasi più cifre di quante ne stampi di norma e l’approssimazione salta fuori:
>>> 0.1
0.1
>>> f"{0.1:.20f}"
'0.10000000000000000555'
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
(0.1).toFixed(20); // '0.10000000000000000555'
(0.1).toPrecision(20); // '0.10000000000000000555'
Decimal(0.1) è la vista onesta: converte il double che hai già, senza riarrotondare, e stampa tutte e 55 le cifre. Il pannello di accuratezza del convertitore IEEE 754 stampa la stessa espansione per qualsiasi numero digiti, accanto all’errore di arrotondamento con segno rispetto a ciò che hai inserito.
L’aritmetica esatta dietro 0.30000000000000004
Resta da capire perché il risultato è proprio quel numero e non uno degli altri valori lì accanto.
Somma esattamente i due valori memorizzati, senza arrotondamenti, e ottieni:
0.1 → 0.1000000000000000055511151231257827021181583404541015625
0.2 → 0.200000000000000011102230246251565404236316680908203125
sum → 0.3000000000000000166533453693773481063544750213623046875
Quella somma non è a sua volta un double rappresentabile. Sta tra due vicini sulla griglia binaria:
below: 0.299999999999999988897769753748434595763683319091796875 (prints as 0.3)
above: 0.3000000000000000444089209850062616169452667236328125 (prints as 0.30000000000000004)
Ora misura le distanze e salta fuori qualcosa di insolito. La somma esatta sta 2⁻⁵⁵ ≈ 2.776e-17 sopra il vicino inferiore e 2⁻⁵⁵ sotto quello superiore, quindi non è più vicina a nessuno dei due: cade esattamente a metà, in perfetto pareggio.
L’arrotondamento al più vicino qui non ha distanze su cui lavorare, quindi IEEE 754 applica il suo criterio di spareggio: round-half-to-even, ovvero scegliere il candidato il cui ultimo bit di mantissa è 0. Il significando del vicino inferiore è 5404319552844595, che è dispari. Quello del superiore è 5404319552844596, che è pari. Vince il valore pari, e dà il pattern di bit 0x3FD3333333333334, ovvero un ULP sopra il double a cui corrisponde il letterale 0.3, stampato come 0.30000000000000004.
Perché preferire il pari? Arrotondare sempre le metà verso l’alto introdurrebbe una distorsione in una sola direzione nelle somme lunghe. Alternare verso il vicino pari tiene quella deriva vicino allo zero su molte operazioni. È lo stesso arrotondamento bancario che usano i contabili, standardizzato nel silicio, ed è la ragione diretta per cui l’errore di arrotondamento in virgola mobile più famoso di internet finisce in ...04.
Confrontalo con una somma che non richiede alcun arrotondamento:
0.5 + 0.25 === 0.75; // true
0.1 + 0.2 === 0.3; // false
0.5, 0.25 e 0.75 sono 2⁻¹, 2⁻² e 2⁻¹ + 2⁻². Tutti e tre sono esatti, la loro somma è esatta e l’uguaglianza si comporta come suggerisce l’aritmetica delle elementari. Il pannello dei vicini del convertitore mostra il valore rappresentabile precedente e quello successivo attorno a ciò che inserisci, insieme al divario in ULP, così puoi controllare la griglia attorno a 0.3 di persona.
Perché il tuo linguaggio a volte nasconde l’errore
0.1 viene stampato come 0.1, ma 0.1 + 0.2 stampa diciassette cifre. Il formato di memorizzazione è identico nei due casi, quindi la differenza nasce da come il runtime decide quante cifre mostrare.
I runtime moderni usano la formattazione shortest round-trip: emettono la stringa decimale più corta che, riletta, produce lo stesso identico double. "0.1" fa già round-trip in modo univoco verso il valore memorizzato per 0.1, quindi è quello che vedi. La somma 0.1 + 0.2 è un double diverso da quello a cui corrisponde "0.3", quindi il formattatore deve continuare ad aggiungere cifre finché la stringa non è priva di ambiguità, e ne servono tutte e diciassette. Il lavoro di David Gay del 1990 sull’arrotondamento corretto ha aperto questa strada; Grisu e poi Ryu l’hanno reso abbastanza veloce per ogni libreria standard. Il tuo linguaggio non ti sta mentendo. Mostra l’etichetta più corta che identifica il valore in modo univoco.
Comportamento linguaggio per linguaggio
| Linguaggio | Tipo float predefinito | 0.1 + 0.2 stampa | Opzione decimale esatta |
|---|---|---|---|
| JavaScript | number (solo double) | 0.30000000000000004 | Nessuna integrata — centesimi interi o una libreria |
| Python | float (double) | 0.30000000000000004 | decimal.Decimal, fractions.Fraction |
| Java | double | 0.30000000000000004 | BigDecimal (costruiscilo da una stringa) |
| C# | double | 0.30000000000000004 | decimal — 128 bit, base 10 |
| Go | float64 | 0.30000000000000004 (tramite variabili) | math/big.Rat |
| Rust | f64 | 0.30000000000000004 | crate rust_decimal |
| SQL | FLOAT / DOUBLE PRECISION | 0.30000000000000004 | NUMERIC / DECIMAL |
Due di quelle righe nascondono una trappola.
Go applica il constant folding a precisione arbitraria. Un’espressione letterale viene valutata esattamente a tempo di compilazione e solo dopo convertita, quindi la costante 0.1 + 0.2 diventa esattamente 0.3 prima ancora di diventare un float64:
package main
import "fmt"
func main() {
fmt.Println(0.1 + 0.2) // 0.3 — constant folded exactly, then converted
a, b := 0.1, 0.2
fmt.Println(a + b) // 0.30000000000000004
fmt.Println(a+b == 0.3) // false
}
Il BigDecimal di Java eredita l’errore se gli passi un double. Il costruttore converte fedelmente i bit che riceve:
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3
SQL si divide per tipo di colonna, non per dialetto:
-- PostgreSQL: a bare decimal literal is NUMERIC, which is exact
SELECT 0.1 + 0.2 = 0.3; -- t
-- Cast to binary floating point and equality fails
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8; -- f
-- SQLite: REAL is a double, and the printed value hides it
SELECT 0.1 + 0.2; -- 0.3
SELECT 0.1 + 0.2 = 0.3; -- 0 (false)
Fissa un attimo quella coppia SQLite. Il risultato stampato dice 0.3, il confronto dice il contrario, ed entrambi hanno ragione.
Confrontare i float: perché == fallisce e Number.EPSILON non è una tolleranza
0.1 + 0.2 === 0.3 è falso perché il lato sinistro è un double diverso. Il consiglio abituale è confrontare con una tolleranza, e la versione più ripetuta di quel consiglio è sbagliata:
Math.abs(a - b) < Number.EPSILON; // ⚠️ not a general-purpose comparison
Number.EPSILON vale 2.220446049250313e-16, cioè 2⁻⁵². MDN lo definisce come la differenza tra 1 e il più piccolo double maggiore di 1. Rileggi con calma: è la spaziatura della griglia a 1.0, non un budget di errore universale. La spaziatura dei float raddoppia a ogni potenza di due, quindi una soglia costante è sbagliata ovunque tranne che nei dintorni in cui è stata misurata.
Vicino a 1.0 funziona per caso:
Math.abs((0.1 + 0.2) - 0.3); // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
Scala lo stesso calcolo di un miliardo di volte e crolla:
const a = (0.1 + 0.2) * 1e9;
const b = 0.3 * 1e9;
a === b; // false
Math.abs(a - b); // 5.960464477539063e-8
Math.abs(a - b) < Number.EPSILON; // false — yet a and b are adjacent doubles
Attorno a 1e9 la spaziatura tra double vicini è circa 1.19e-7. Dato che 1e9 cade in [2²⁹, 2³⁰), lì l’ULP vale 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7, circa 500 milioni di volte più grande di Number.EPSILON. Qualsiasi errore di arrotondamento reale a quella magnitudine schiaccia la soglia, quindi il controllo restituisce falso per sempre. Giù a 1e-20 la stessa costante è di gran lunga troppo generosa e dichiara uguali valori palesemente diversi.
Tolleranza assoluta, relativa e in ULP
Gli strumenti sono tre e servono a cose diverse:
- Tolleranza assoluta:
|a − b| <= atol. Giusta quando conosci in anticipo l’ordine di grandezza, ed è l’unica che funziona contro lo zero, dato che una tolleranza relativa attorno a 0 vale sempre 0. - Tolleranza relativa:
|a − b| <= rtol × max(|a|, |b|). Scala con i dati su tutte le magnitudini, ma degenera vicino allo zero. - Distanza in ULP: quanti valori rappresentabili stanno tra i due pattern di bit. È la più precisa e la meno leggibile.
Combina le prime due e ottieni la forma che usa ogni libreria numerica seria:
function nearlyEqual(a, b, rtol = 1e-9, atol = 1e-12) {
if (a === b) return true; // handles Infinity === Infinity
const diff = Math.abs(a - b);
return diff <= Math.max(rtol * Math.max(Math.abs(a), Math.abs(b)), atol);
}
nearlyEqual(0.1 + 0.2, 0.3); // true
nearlyEqual((0.1 + 0.2) * 1e9, 0.3 * 1e9); // true
nearlyEqual(0, 1e-15); // true
nearlyEqual(1, 1.0001); // false
Python te la offre nella libreria standard, e allclose di NumPy usa la stessa formula:
import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12) # True
math.isclose(1e9, 1e9 + 1e-7, rel_tol=1e-9) # True
Per la versione rigorosa, mappa i bit su un ordinamento intero monotono e sottrai:
const view = new DataView(new ArrayBuffer(8));
function toOrdinal(x) {
view.setFloat64(0, x);
const i = view.getBigInt64(0);
return i >= 0n ? i : -(1n << 63n) - i; // make negatives order correctly
}
function ulpDistance(a, b) {
const d = toOrdinal(a) - toOrdinal(b);
return d < 0n ? -d : d;
}
ulpDistance(0.1 + 0.2, 0.3); // 1n
ulpDistance((0.1 + 0.2) * 1e9, 0.3 * 1e9); // 1n
ulpDistance(-0, 0); // 0n
Tra l’altro, l’== esatto non è sempre sbagliato. Va benissimo per i double con valore intero sotto 2⁵³, per le costanti che hai assegnato e su cui non hai mai calcolato, e per controllare se un valore è esattamente zero.
Quando la precisione costa denaro: scegliere il tipo numerico giusto
Valuta e float binari sono una brutta accoppiata, e non perché un singolo errore sia grande. Gli errori sono sistematici, riproducibili e invisibili finché non li trova un revisore contabile:
19.99 * 100; // 1998.9999999999998
Math.round(19.99 * 100); // 1999
(1.005).toFixed(2); // '1.00' — 1.005 is stored as 1.00499999999999989...
Il secondo caso frega qualche team ogni anno. toFixed non ha arrotondato male. Il valore che gli è arrivato era già sotto 1.005.
Unità minori intere
Memorizza i centesimi, non i dollari. 1999 significa $19.99, l’aritmetica gira sugli interi e dividi solo al momento di mostrare il valore.
const priceCents = 1999; // $19.99
const subtotal = priceCents * 3; // 5997 — exact
const totalCents = 1000; // $10.00 split three ways
const share = Math.floor(totalCents / 3); // 333
const remainder = totalCents - share * 3; // 1 cent to allocate
Due avvertenze. Sopra Number.MAX_SAFE_INTEGER (9007199254740991, ovvero 2⁵³ − 1) gli interi JavaScript smettono di essere esatti, quindi usa BigInt. E gli interi non decidono la tua politica di arrotondamento: divisioni, percentuali e ripartizioni fiscali richiedono comunque una regola esplicita su dove finisce il centesimo avanzato.
Tipi decimali
Un tipo decimale memorizza cifre in base 10, quindi le frazioni decimali cadono esatte. Costruiscilo da una stringa, mai da un float, altrimenti erediti l’errore binario prima ancora di iniziare:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
Decimal(0.1) # 0.1000000000000000055511151231257827021181583404541015625
Console.WriteLine(0.1m + 0.2m); // 0.3
Console.WriteLine(0.1m + 0.2m == 0.3m); // True
Java ha BigDecimal, C# un decimal nativo a 128 bit, SQL un NUMERIC(12,2). Tutti sistemano le frazioni decimali. Nessuno sistema 1/3, perché un tipo decimale resta comunque un formato in virgola mobile; ha solo spostato la base da 2 a 10.
Matrice decisionale
| Caso d’uso | Tipo consigliato | Perché |
|---|---|---|
| Denaro, fatturazione, tasse | Unità minori intere o decimal | Aritmetica decimale esatta, arrotondamento verificabile |
| Scienza, fisica | double (FP64) | 15–16 cifre bastano e avanzano; miglior supporto nelle librerie |
| Geometria, grafica | float o double + tolleranza | Già approssimato; confronta con epsilon |
| Addestramento ML | bfloat16 | La gamma di FP32 con metà della memoria |
| Inferenza ML, archiviazione | FP16 | Più bit di mantissa quando i valori sono normalizzati |
| Contatori, ID, chiavi | Intero a 64 bit o BigInt | Non sono mai appartenuti a un float |
Accumulo degli errori e cancellazione catastrofica
Un errore di arrotondamento vale 1e-17 ed è innocuo. Diecimila sono un ticket di assistenza:
let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;
total; // 1000.0000000001588
total - 1000; // 1.588205122970976e-10
La deriva cresce all’incirca con il numero di operazioni. In un ciclo di queste dimensioni è invisibile; in un’aggregazione notturna su milioni di righe non lo è affatto.
Cancellazione catastrofica
Il guasto peggiore è la sottrazione. Sottrai due numeri quasi uguali e le cifre iniziali che hanno in comune si annullano; quello che resta è dominato dall’errore che si era già accumulato. L’errore assoluto non cresce. Esplode l’errore relativo, perché ora il risultato è minuscolo mentre l’errore è rimasto della stessa dimensione.
La formula da manuale per la varianza in un solo passaggio, E[x²] − (E[x])², ci finisce dentro in pieno:
const data = [1e9, 1e9 + 1, 1e9 + 2];
const mean = data.reduce((s, x) => s + x, 0) / data.length;
// One-pass: E[x²] − (E[x])²
data.reduce((s, x) => s + x * x, 0) / data.length - mean * mean; // 0
// Two-pass: centre the data first
data.reduce((s, x) => s + (x - mean) ** 2, 0) / data.length; // 0.6666666666666666
La vera varianza di popolazione è 2/3. La formula a un passaggio restituisce esattamente zero: non un valore leggermente sbagliato, ma una risposta del tutto diversa, perché entrambi gli operandi stavano attorno a 1e18 e la differenza tra loro vive sotto l’ultimo bit memorizzato. L’algoritmo online di Welford schiva il problema aggiornando la media e la somma degli scarti quadratici in modo incrementale.
La formula risolutiva di secondo grado ha la stessa debolezza quando b² ≫ 4ac, e lo stesso tipo di rimedio:
const a = 1, b = 1e8, c = 1;
const disc = Math.sqrt(b * b - 4 * a * c);
(-b + disc) / (2 * a); // -7.450580596923828e-9 — about 25% wrong
(2 * c) / (-b - disc); // -1e-8 — correct
Entrambe le righe calcolano la stessa radice. La prima sottrae due numeri quasi uguali; la seconda riorganizza l’algebra in modo da non doverlo mai fare. What Every Computer Scientist Should Know About Floating-Point Arithmetic di Goldberg resta il testo di riferimento su cancellazione e limiti d’errore.
Somma di Kahan
La somma compensata di Kahan tiene traccia dei bit di basso ordine che ogni addizione butta via, poi li rimette in gioco all’iterazione successiva:
function kahanSum(values) {
let sum = 0;
let compensation = 0;
for (const value of values) {
const y = value - compensation;
const t = sum + y;
compensation = (t - sum) - y; // the bits that fell off
sum = t;
}
return sum;
}
kahanSum(new Array(10000).fill(0.1)); // 1000 — exactly
Python te la regala. math.fsum([0.1] * 10_000) restituisce 1000.0, e da CPython 3.12 anche la sum() integrata usa la compensazione di Neumaier, quindi sum([0.1] * 10_000) è esatto mentre un ciclo esplicito con += deriva ancora a 1000.0000000001588.
Ricorri alla compensazione per somme lunghe, aggregati finanziari e integrazione numerica. Saltala per una manciata di valori o quando sei già passato a un tipo esatto.
I valori speciali che rompono le assunzioni
IEEE 754 riserva pattern di bit per valori che non obbediscono alle regole che ti aspetti:
NaN === NaN; // false — mandated by the standard
Number.isNaN(NaN); // true
[NaN].indexOf(NaN); // -1 (uses ===)
[NaN].includes(NaN); // true (uses SameValueZero)
-0 === 0; // true
Object.is(-0, 0); // false
1 / -0; // -Infinity
1e308 * 10; // Infinity — overflow never throws
Infinity - Infinity; // NaN
Number.MIN_VALUE; // 5e-324 — the smallest subnormal double
NaN risulta diverso da tutto, se stesso incluso, ed è voluto: così un calcolo fallito non può mai spacciarsi per un risultato valido. Testalo con Number.isNaN() o con math.isnan() di Python.
L’overflow è più silenzioso e più pericoloso: restituisce ±Infinity e tira dritto, propagandosi a valle finché qualcuno non nota un grafico pieno di buchi. Lo zero negativo è un pattern di bit distinto che sotto === risulta comunque uguale a +0; compare dall’underflow di un valore negativo o da -1 * 0, e solo la divisione o Object.is lo rivelano.
I subnormali riempiono lo spazio tra lo zero e il più piccolo numero normale. Con il campo dell’esponente tutto a zero l’1 iniziale implicito cade e la mantissa si restringe gradualmente verso lo zero, il che garantisce che la differenza tra due float diversi non arrotondi mai esattamente a zero. Il prezzo è una precisione calante e, su certo hardware, un crollo netto delle prestazioni. I chip dei valori speciali nel convertitore IEEE 754 caricano ±0, ±Infinity, NaN e il più piccolo subnormale, così puoi ispezionare direttamente ogni pattern di bit.
float vs double vs FP16 vs bfloat16
Lo standard è sempre lo stesso: cambia solo come i bit vengono ripartiti tra gamma e precisione in virgola mobile.
| Formato | Bit totali | Esponente | Mantissa | Cifre decimali approx. | Massimo finito | Uso tipico |
|---|---|---|---|---|---|---|
| binary16 (FP16) | 16 | 5 | 10 | ~3.3 | 65504 | Inferenza su GPU, archiviazione compatta |
| bfloat16 | 16 | 8 | 7 | ~2.4 | ~3.39 × 10³⁸ | Addestramento ML |
| binary32 (float) | 32 | 8 | 23 | ~7.2 | ~3.40 × 10³⁸ | Grafica, sensori, GPU |
| binary64 (double) | 64 | 11 | 52 | ~15.9 | ~1.80 × 10³⁰⁸ | Predefinito in tutto il resto |
I bit di esponente comprano gamma; i bit di mantissa comprano precisione. FP16 spende il suo budget in precisione e si ferma a 65504, un tetto abbastanza vicino ai gradienti reali da rendere l’overflow a Infinity un rischio di ordinaria amministrazione. bfloat16 fa lo scambio opposto: tiene gli otto bit di esponente di FP32 e si accontenta di sette bit di mantissa, così i valori che manderebbero FP16 in overflow passano lisci e l’addestramento raramente ha bisogno del loss scaling.
Parti dal double. Scendi a un formato più stretto dopo aver misurato un collo di bottiglia di memoria o di banda, e controlla quanto costa il restringimento inserendo lo stesso valore in ciascun formato nel convertitore.
Regole pratiche per la precisione in virgola mobile
- Non usare mai
==su float calcolati. Usa una tolleranza combinata, assoluta più relativa, dimensionata sui tuoi dati. - Non trattare
Number.EPSILONcome una soglia. Descrive la griglia a 1.0 e nient’altro. - Tieni il denaro in unità minori intere o in un tipo decimale, e costruisci sempre i decimali da stringhe.
- Compensa le somme lunghe con Kahan, Neumaier o
math.fsum. - Riscrivi le formule che sottraggono quantità quasi uguali invece di stringere le tolleranze attorno a esse.
- Formatta esplicitamente per le persone con
toFixed, una f-string oprintf. Non mandare mai unreprpredefinito agli utenti. - Manda i numeri tra un servizio e l’altro come stringhe o interi. Un round-trip JSON attraverso un double è silenzioso e con perdita.
- Scegli
NUMERIC, nonFLOAT, per le colonne di valuta. Sistemarlo dopo il lancio significa una migrazione e una riconciliazione. - Ragiona in bit quando un valore sembra impossibile. È la stessa abitudine che sta dietro la nostra guida alle operazioni bit a bit, e riporta i misteri della virgola mobile a un’aritmetica che puoi verificare a mano.
Domande frequenti
La matematica in virgola mobile è rotta?
La matematica in virgola mobile non è rotta: segue IEEE 754 alla lettera. Lo standard rappresenta i numeri reali con un numero finito di cifre binarie, e le frazioni decimali come 0.1 non hanno forma binaria esatta, quindi viene memorizzato il valore rappresentabile più vicino. La parte rotta è l’aspettativa che ogni decimale ci entri.
Perché Python stampa 0.1 come 0.1 ma 0.1 + 0.2 come 0.30000000000000004?
Il repr di Python usa la formattazione shortest round-trip: stampa la stringa decimale più corta che, riletta, produce lo stesso identico double. "0.1" identifica già il suo double in modo univoco. La somma 0.1 + 0.2 è un double diverso da quello a cui corrisponde "0.3", quindi il formattatore deve emettere tutte e diciassette le cifre per restare senza ambiguità.
Perché non dovrei usare Number.EPSILON per confrontare due float?
Number.EPSILON (circa 2.22e-16) è il divario tra 1 e il double successivo, non un budget di errore universale. La spaziatura dei float raddoppia a ogni potenza di due, quindi vicino a 1e9 i double contigui distano circa 1.19e-7. Qualsiasi errore di arrotondamento reale lì supera EPSILON e il confronto risulta sempre falso. Usa una tolleranza relativa o combinata.
Posso usare la virgola mobile per il denaro se arrotondo alla fine?
Arrotondare alla fine non salva il denaro in virgola mobile. Gli errori si accumulano tra addizioni, moltiplicazioni e ripartizioni a più passaggi, e il momento in cui arrotondi cambia il totale. 19.99 * 100 dà già 1998.9999999999998. Per la revisione contabile serve aritmetica esatta e riproducibile: centesimi interi, decimal, BigDecimal o NUMERIC di SQL.
Quali numeri decimali si possono memorizzare esattamente in un double?
Solo i valori esprimibili come m / 2ⁿ con m e n interi entro la precisione del formato. Rientrano 0.5, 0.25, 0.125, 0.75 e 2.5, più ogni intero fino a 2⁵³. Riduci prima la frazione: qualsiasi fattore 5 che resta al denominatore, come in 0.1, 0.2, 0.3 o 0.7, significa che il valore viene approssimato.
Perché 0.1 + 0.2 === 0.3 è falso ma 0.5 + 0.25 === 0.75 è vero?
Perché 0.5, 0.25 e 0.75 sono 2⁻¹, 2⁻² e 2⁻¹ + 2⁻², tutti esattamente rappresentabili, e la loro somma non richiede alcun arrotondamento. 0.1, 0.2 e 0.3 vengono invece memorizzati in modo approssimato, e arrotondare la somma dei primi due porta un ULP sopra il double a cui corrisponde 0.3.
Succede in tutti i linguaggi di programmazione?
Ogni linguaggio costruito sulla virgola mobile binaria IEEE 754 mostra lo stesso comportamento: JavaScript, Python, Java, C, C++, Go, Rust, Swift e il FLOAT di SQL. Cambiano la precisione di stampa predefinita e la presenza o meno di un tipo decimale esatto già incluso, come il decimal di C# o il modulo decimal di Python.