Precisão de ponto flutuante: por que 0.1 + 0.2 ≠ 0.3
0.1 + 0.2 devolve 0.30000000000000004 porque nem 0.1 nem 0.2 existem dentro de um número binário de ponto flutuante. Um double só consegue guardar valores da forma m × 2ⁿ, e um décimo é uma dízima periódica infinita na base 2, então o hardware guarda o vizinho representável mais próximo. O double por trás de 0.1 é exatamente 0.1000000000000000055511151231257827021181583404541015625. O que está por trás de 0.2 é 0.200000000000000011102230246251565404236316680908203125. Some esses dois valores armazenados e a soma exata cai entre dois doubles representáveis. O IEEE 754 arredonda para o mais próximo, que fica um fio de cabelo acima de 0.3.
É essa a resposta inteira: a precisão de ponto flutuante é finita e frações decimais raramente cabem na grade binária. Cada operação seguinte arredonda de novo.
Isso não é uma esquisitice do JavaScript nem um defeito da CPU. O mesmo resultado aparece em Python, Java, C, Go, Rust, Swift e nas colunas FLOAT do SQL, porque todos rodam sobre o IEEE 754. Cole qualquer valor no conversor IEEE 754 gratuito para ver os bits exatos e o valor armazenado exato, dígito por dígito.
Daqui em diante: por que o erro aparece, por que a sua linguagem às vezes o esconde, como comparar floats sem == e qual tipo numérico usar quando precisão custa dinheiro.
Precisão de ponto flutuante em 60 segundos
Três fatos explicam quase toda surpresa:
- O ponto flutuante binário só representa números da forma m × 2ⁿ, ou seja, somas de potências de dois.
- Frações decimais como
0.1,0.2e0.3não têm essa forma, então são arredondadas já na entrada. - Cada operação aritmética arredonda o resultado outra vez para o valor representável mais próximo.
| Decimal | Representável exatamente? | Por quê |
|---|---|---|
| 0.5 | Sim | 2⁻¹ |
| 0.25 | Sim | 2⁻² |
| 0.75 | Sim | 2⁻¹ + 2⁻² |
| 2.5 | Sim | 2 + 2⁻¹ |
| 100 | Sim | Qualquer inteiro abaixo de 2⁵³ |
| 0.1 | Não | 1/10, e o denominador tem um fator 5 |
| 0.2 | Não | 1/5, mesmo problema |
| 0.3 | Não | 3/10, mesmo problema |
Regra prática: simplifique a fração; se o denominador for uma potência pura de dois, o valor é exato. Qualquer fator 5 que sobreviver significa que o valor fica armazenado de forma aproximada.
Por que 0.1 não pode ser armazenado com exatidão
Os bits depois do ponto binário carregam os pesos 1/2, 1/4, 1/8, 1/16, 1/32 e assim por diante. Tente montar 0.1 com eles e você nunca chega lá. Sai 1/16 = 0.0625, mais 1/32 = 0.09375, mais 1/256 = 0.09765625. Cada termo chega mais perto sem nunca acertar o valor. Em binário, um décimo é 0.0001100110011… com 0011 se repetindo para sempre.
O sistema decimal tem o mesmo problema, só que com outras frações. Escrever 1/3 na base 10 dá 0.333… e nenhuma sequência finita de dígitos acerta o valor. Ninguém chama isso de bug do decimal. A base 2 apenas traça a linha em outro lugar, e 1/10 calha de cair do lado errado dela. A documentação do Python, em Floating Point Arithmetic: Issues and Limitations, percorre a mesma expansão se você quiser a versão longa.
Binário é só a base 2, e a mesma aritmética posicional move o hexadecimal e o octal. O conversor de base numérica mostra como um valor migra entre bases, e o nosso guia de conversão de bases cobre o lado dos inteiros em profundidade. As frações são onde a base 2 começa a doer.
O que um double realmente armazena
Um double de 64 bits se divide em três campos: 1 bit de sinal, 11 bits de expoente e 52 bits de mantissa. A mantissa carrega um 1 inicial implícito que nunca é armazenado, então você fica com 53 bits de significando, algo em torno de 15.95 dígitos decimais de precisão de ponto flutuante. O expoente é guardado com um viés de 1023.
Peça a qualquer linguagem mais dígitos do que ela costuma imprimir e a aproximação aparece:
>>> 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) é a visão honesta: ele converte o double que você já tem, sem rearredondar, e imprime todos os 55 dígitos. O painel de precisão do conversor IEEE 754 imprime a mesma expansão para qualquer número que você digitar, ao lado do erro de arredondamento com sinal em relação ao que você inseriu.
A aritmética exata por trás de 0.30000000000000004
Falta a parte interessante: por que o resultado é aquele número específico e não outro quase acerto qualquer.
Some os dois valores armazenados de forma exata, sem nenhum arredondamento, e você obtém:
0.1 → 0.1000000000000000055511151231257827021181583404541015625
0.2 → 0.200000000000000011102230246251565404236316680908203125
sum → 0.3000000000000000166533453693773481063544750213623046875
Essa soma não é, ela mesma, um double representável. Ela fica entre dois vizinhos na grade binária:
below: 0.299999999999999988897769753748434595763683319091796875 (prints as 0.3)
above: 0.3000000000000000444089209850062616169452667236328125 (prints as 0.30000000000000004)
Agora meça as distâncias e algo incomum aparece. A soma exata está 2⁻⁵⁵ ≈ 2.776e-17 acima do vizinho de baixo e 2⁻⁵⁵ abaixo do de cima. Ou seja, não está mais perto de nenhum dos dois: cai exatamente no meio, num empate perfeito.
O arredondamento para o mais próximo não tem distância nenhuma com que trabalhar aqui, então o IEEE 754 aplica seu critério de desempate: arredondar a metade para o par (round-half-to-even), ou seja, escolher o candidato cujo último bit de mantissa seja 0. O significando do vizinho de baixo é 5404319552844595, que é ímpar. O do de cima é 5404319552844596, que é par. O valor par vence e produz o padrão de bits 0x3FD3333333333334, um ULP acima do double para o qual o literal 0.3 mapeia, e impresso como 0.30000000000000004.
Por que preferir o par? Arredondar sempre as metades para cima enviesaria somas longas em uma direção só. Alternar em direção ao vizinho par mantém esse desvio perto de zero ao longo de muitas operações. É o mesmo arredondamento bancário que os contadores usam, padronizado em silício, e é a razão direta de o erro de arredondamento de ponto flutuante mais famoso da internet terminar em ...04.
Compare com uma soma que não precisa de arredondamento nenhum:
0.5 + 0.25 === 0.75; // true
0.1 + 0.2 === 0.3; // false
0.5, 0.25 e 0.75 são 2⁻¹, 2⁻² e 2⁻¹ + 2⁻². Os três são exatos, a soma deles também, e a igualdade se comporta como a aritmética da escola sugere. O painel de vizinhos do conversor mostra os valores representáveis anterior e seguinte ao redor do que você digitar, junto com o intervalo de um ULP, para você mesmo conferir a grade em torno de 0.3.
Por que a sua linguagem às vezes esconde o erro
É isto que dá ao assunto inteiro um ar de assombração: 0.1 imprime como 0.1, mas 0.1 + 0.2 imprime dezessete dígitos. O formato de armazenamento é o mesmo; as saídas não se parecem em nada.
Os runtimes modernos usam a formatação de ida e volta mais curta (shortest round-trip): emitem a menor string decimal que faz o parse de volta para o double idêntico. "0.1" já faz esse percurso de forma única até o valor armazenado para 0.1, então é isso que você vê. A soma 0.1 + 0.2 é um double diferente daquele para o qual "0.3" mapeia, então o formatador precisa acrescentar dígitos até a string ficar inequívoca, e isso consome os dezessete. O trabalho de arredondamento correto de David Gay, em 1990, iniciou essa linhagem; Grisu e depois Ryu a deixaram rápida o bastante para qualquer biblioteca padrão. A sua linguagem não está mentindo. Ela mostra o rótulo mais curto que identifica o valor de forma única.
Comportamento linguagem por linguagem
| Linguagem | Tipo float padrão | 0.1 + 0.2 imprime | Opção decimal exata |
|---|---|---|---|
| JavaScript | number (só double) | 0.30000000000000004 | Nenhuma nativa; centavos inteiros ou biblioteca |
| Python | float (double) | 0.30000000000000004 | decimal.Decimal, fractions.Fraction |
| Java | double | 0.30000000000000004 | BigDecimal (construa a partir de uma string) |
| C# | double | 0.30000000000000004 | decimal nativo de 128 bits, base 10 |
| Go | float64 | 0.30000000000000004 (via variáveis) | math/big.Rat |
| Rust | f64 | 0.30000000000000004 | crate rust_decimal |
| SQL | FLOAT / DOUBLE PRECISION | 0.30000000000000004 | NUMERIC / DECIMAL |
Duas dessas linhas escondem uma armadilha.
O Go faz constant folding com precisão arbitrária. Uma expressão literal é avaliada de forma exata em tempo de compilação e só depois convertida, então a constante 0.1 + 0.2 vira exatamente 0.3 antes mesmo de virar um 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
}
O BigDecimal do Java herda o erro se você alimentá-lo com um double. O construtor converte fielmente os bits que recebeu:
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3
O SQL se divide por tipo de coluna, e não por dialeto:
-- 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)
Olhe esse par do SQLite com calma. O resultado impresso diz 0.3, a comparação diz o contrário, e os dois estão certos.
Comparando floats: por que == falha e Number.EPSILON não é uma tolerância
0.1 + 0.2 === 0.3 é falso porque o lado esquerdo é um double diferente. O conselho de sempre é comparar com uma tolerância, e a versão mais repetida dele está errada:
Math.abs(a - b) < Number.EPSILON; // ⚠️ not a general-purpose comparison
Number.EPSILON é 2.220446049250313e-16, ou seja, 2⁻⁵². O MDN o define como a diferença entre 1 e o menor double maior que 1. Leia de novo: é o espaçamento da grade em 1.0, não um orçamento universal de erro. O espaçamento dos floats dobra a cada potência de dois, então um limiar constante está errado em toda parte, menos na vizinhança em que foi medido.
Perto de 1.0 ele por acaso funciona:
Math.abs((0.1 + 0.2) - 0.3); // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
Amplie o mesmo cálculo por um bilhão e ele desmorona:
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
Em torno de 1e9, o espaçamento entre doubles vizinhos é de aproximadamente 1.19e-7. Como 1e9 cai em [2²⁹, 2³⁰), o ULP ali é 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7, cerca de 500 milhões de vezes maior que Number.EPSILON. Qualquer erro de arredondamento real nessa magnitude engole o limiar, então a checagem retorna false para sempre. Lá embaixo, em 1e-20, a mesma constante é generosa demais e declara iguais valores obviamente diferentes.
Tolerância absoluta, relativa e em ULP
São três ferramentas com finalidades diferentes:
- A tolerância absoluta,
|a − b| <= atol, serve quando você conhece a magnitude de antemão, e é a única que funciona contra o zero, já que uma tolerância relativa em torno de 0 é sempre 0. - A tolerância relativa,
|a − b| <= rtol × max(|a|, |b|), acompanha a escala dos dados em várias magnitudes, mas degenera perto de zero. - A distância em ULP conta quantos valores representáveis cabem entre os dois padrões de bits. É a mais precisa e também a menos legível.
Combine as duas primeiras e você chega ao formato que as bibliotecas numéricas sérias usam:
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
O Python já traz isso na biblioteca padrão, e o allclose do NumPy usa a mesma fórmula:
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
Para a versão estrita, mapeie os bits para uma ordenação inteira monotônica e subtraia:
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
A propósito, o == exato nem sempre está errado. Ele serve bem para doubles de valor inteiro abaixo de 2⁵³, para constantes que você atribuiu e sobre as quais nunca calculou nada, e para checar se um valor é exatamente zero.
Quando a precisão custa dinheiro: escolhendo o tipo numérico certo
Dinheiro e floats binários combinam mal, e não porque algum erro isolado seja grande. Os erros são sistemáticos e reproduzíveis, e ficam invisíveis até um auditor tropeçar neles:
19.99 * 100; // 1998.9999999999998
Math.round(19.99 * 100); // 1999
(1.005).toFixed(2); // '1.00' — 1.005 is stored as 1.00499999999999989...
O segundo caso pega equipes desprevenidas todo ano. O toFixed não arredondou errado. O valor entregue a ele já estava abaixo de 1.005.
Unidades monetárias inteiras
Guarde centavos, não a moeda inteira. 1999 significa $19.99, a aritmética roda sobre inteiros, e você divide só na hora de exibir.
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
Duas ressalvas. Acima de Number.MAX_SAFE_INTEGER (9007199254740991, ou 2⁵³ − 1) os inteiros do JavaScript deixam de ser exatos, então use BigInt. E inteiros não decidem a sua política de arredondamento: divisões, porcentagens e rateios de imposto continuam precisando de uma regra explícita para onde vai o centavo que sobra.
Tipos decimais
Um tipo decimal armazena dígitos na base 10, então frações decimais caem exatamente na grade. Construa-o a partir de uma string, nunca de um float, ou você herda o erro binário antes mesmo de começar:
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 tem BigDecimal, C# tem um decimal nativo de 128 bits, o SQL tem NUMERIC(12,2). Todos resolvem as frações decimais. Nenhum resolve 1/3, porque um tipo decimal continua sendo um formato de ponto flutuante; ele só mudou a base de 2 para 10.
Matriz de decisão
| Caso de uso | Tipo recomendado | Por quê |
|---|---|---|
| Dinheiro, faturamento, imposto | Unidades monetárias inteiras ou decimal | Aritmética decimal exata, arredondamento auditável |
| Ciência, física | double (FP64) | 15–16 dígitos são de sobra; melhor suporte de bibliotecas |
| Geometria, gráficos | float ou double + tolerância | Já é aproximado; compare com epsilon |
| Treinamento de ML | bfloat16 | O alcance do FP32 com metade da memória |
| Inferência de ML, armazenamento | FP16 | Mais bits de mantissa quando os valores estão normalizados |
| Contadores, IDs, chaves | Inteiro de 64 bits ou BigInt | Nunca pertenceram a um float |
Acúmulo de erro e cancelamento catastrófico
Um erro de arredondamento é 1e-17 e inofensivo. Dez mil deles viram um chamado no suporte:
let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;
total; // 1000.0000000001588
total - 1000; // 1.588205122970976e-10
O desvio cresce mais ou menos junto com o número de operações. Em um laço desse tamanho ele é invisível; em uma agregação noturna sobre milhões de linhas, não é.
Cancelamento catastrófico
A falha mais desagradável é a subtração. Subtraia dois números quase iguais e os dígitos iniciais coincidentes se cancelam, deixando um resultado dominado por qualquer erro que já tivesse se acumulado. O erro absoluto não cresce. O erro relativo explode, porque o resultado agora é minúsculo enquanto o erro continua do mesmo tamanho.
A fórmula de variância em uma passagem dos livros-texto, E[x²] − (E[x])², cai direto nessa armadilha:
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
A variância populacional verdadeira é 2/3. A fórmula de uma passagem devolve exatamente zero, uma resposta completamente diferente e não apenas um resultado meio torto, porque os dois operandos ficavam em torno de 1e18 e a diferença entre eles vive abaixo do último bit armazenado. O algoritmo online de Welford evita isso atualizando a média e a soma dos desvios quadrados de forma incremental.
A fórmula de Bhaskara tem a mesma fraqueza quando b² ≫ 4ac, e o mesmo estilo de correção:
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
As duas linhas calculam a mesma raiz. A primeira subtrai dois números quase iguais; a segunda reorganiza a álgebra para nunca precisar disso. O texto de Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, continua sendo a referência sobre cancelamento e limites de erro.
Somatório de Kahan
O somatório compensado de Kahan mantém um registro corrente dos bits de menor ordem que cada adição descarta e devolve esses bits na iteração seguinte:
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
O Python te dá isso de graça. math.fsum([0.1] * 10_000) devolve 1000.0, e desde o CPython 3.12 a sum() embutida também usa a compensação de Neumaier, então sum([0.1] * 10_000) é exato enquanto um laço explícito com += ainda desliza para 1000.0000000001588.
Recorra à compensação em somas longas, agregados financeiros e integração numérica. Dispense-a para um punhado de valores ou quando você já migrou para um tipo exato.
Os valores especiais que quebram suposições
O IEEE 754 reserva padrões de bits para valores que não obedecem às regras que você espera:
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 compara como diferente de tudo, inclusive de si mesmo, e isso é proposital: um cálculo que falhou nunca pode se passar por um resultado válido. Teste com Number.isNaN() ou com o math.isnan() do Python.
O overflow é mais silencioso e mais perigoso: devolve ±Infinity e segue em frente, e o valor viaja rio abaixo até alguém notar um gráfico cheio de lacunas. O zero negativo é um padrão de bits distinto que mesmo assim é igual a +0 sob ===; ele aparece no underflow de um valor negativo ou em -1 * 0, e só a divisão ou o Object.is o revelam.
Os subnormais preenchem a lacuna entre zero e o menor número normal. Com o campo de expoente todo zerado, o 1 inicial implícito é descartado e a mantissa encolhe gradualmente rumo a zero, o que garante que a diferença entre dois floats distintos nunca arredonde para exatamente zero. O preço é a precisão em queda e, em certos hardwares, um despenhadeiro de desempenho. Os botões de valores especiais do conversor IEEE 754 carregam ±0, ±Infinity, NaN e o menor subnormal para você inspecionar cada padrão de bits diretamente.
float vs double vs FP16 vs bfloat16
É o mesmo padrão com orçamentos diferentes de alcance e de precisão de ponto flutuante:
| Formato | Total de bits | Expoente | Mantissa | Dígitos decimais aprox. | Máximo finito | Uso típico |
|---|---|---|---|---|---|---|
| binary16 (FP16) | 16 | 5 | 10 | ~3.3 | 65504 | Inferência em GPU, armazenamento compacto |
| bfloat16 | 16 | 8 | 7 | ~2.4 | ~3.39 × 10³⁸ | Treinamento de ML |
| binary32 (float) | 32 | 8 | 23 | ~7.2 | ~3.40 × 10³⁸ | Gráficos, sensores, GPU |
| binary64 (double) | 64 | 11 | 52 | ~15.9 | ~1.80 × 10³⁰⁸ | Padrão em todo o resto |
Bits de expoente compram alcance; bits de mantissa compram precisão. O FP16 gasta o orçamento em precisão e para em 65504, perto demais dos valores reais de gradiente: o overflow para Infinity vira risco de rotina. O bfloat16 faz a troca oposta: fica com os oito bits de expoente do FP32 e convive com sete bits de mantissa, então valores que estourariam no FP16 passam sem sustos e o treinamento raramente precisa de loss scaling.
Use double por padrão. Desça para um formato mais estreito depois de ter medido um gargalo de memória ou de banda, e confira o custo desse estreitamento digitando o mesmo valor em cada formato no conversor.
Regras práticas para precisão de ponto flutuante
- Nunca use
==em floats calculados. Prefira uma tolerância combinada, absoluta mais relativa, dimensionada para os seus dados. - Não trate
Number.EPSILONcomo limiar: ele descreve a grade em 1.0 e nada além disso. - Mantenha dinheiro em unidades monetárias inteiras ou em um tipo decimal, e sempre construa decimais a partir de strings.
- Compense somas longas com Kahan, Neumaier ou
math.fsum. - Reescreva fórmulas que subtraem quantidades quase iguais em vez de apertar as tolerâncias em volta delas.
- Formate explicitamente para humanos com
toFixed, uma f-string ouprintf, e nunca entregue umreprpadrão aos usuários. - Envie números entre serviços como strings ou inteiros. Uma ida e volta em JSON passando por um double perde informação em silêncio.
- Escolha
NUMERIC, e nãoFLOAT, para colunas de moeda. Corrigir isso depois do lançamento significa migração e reconciliação. - Pense em bits quando um valor parecer impossível. O mesmo hábito move o nosso guia de operações bit a bit e transforma mistérios de ponto flutuante em aritmética que você consegue conferir.
FAQ
A matemática de ponto flutuante é quebrada?
A matemática de ponto flutuante não é quebrada: ela segue o IEEE 754 à risca. O padrão representa números reais com uma quantidade finita de dígitos binários, e frações decimais como 0.1 não têm forma binária exata, então fica armazenado o valor representável mais próximo. O que está quebrado é a expectativa de que todo decimal caiba.
Por que o Python imprime 0.1 como 0.1, mas 0.1 + 0.2 como 0.30000000000000004?
O repr do Python usa formatação de ida e volta mais curta: ele imprime a menor string decimal que faz o parse de volta para o double idêntico. "0.1" já identifica o seu double de forma única. A soma 0.1 + 0.2 é um double diferente daquele para o qual "0.3" mapeia, então o formatador precisa emitir os dezessete dígitos para não ficar ambíguo.
Por que eu não deveria usar Number.EPSILON para comparar dois floats?
Number.EPSILON (cerca de 2.22e-16) é a distância entre 1 e o próximo double, não um orçamento universal de erro. O espaçamento dos floats dobra a cada potência de dois, então perto de 1e9 dois doubles vizinhos ficam a cerca de 1.19e-7 um do outro. Qualquer erro de arredondamento genuíno ali excede o EPSILON, o que torna a comparação sempre falsa. Use uma tolerância relativa ou combinada.
Posso usar ponto flutuante para dinheiro se eu arredondar no final?
Arredondar no final não salva dinheiro guardado em ponto flutuante. Os erros se acumulam em adições, multiplicações e rateios de várias etapas, e o momento em que você arredonda muda o total. 19.99 * 100 já dá 1998.9999999999998. Auditoria exige aritmética exata e reproduzível: centavos inteiros, decimal, BigDecimal ou NUMERIC do SQL.
Quais números decimais podem ser armazenados exatamente em um double?
Apenas valores expressáveis como m / 2ⁿ para inteiros m e n dentro da precisão do formato. Isso cobre 0.5, 0.25, 0.125, 0.75 e 2.5, além de todo inteiro até 2⁵³. Simplifique a fração primeiro: qualquer fator 5 que reste no denominador, como em 0.1, 0.2, 0.3 ou 0.7, significa que o valor é aproximado.
Por que 0.1 + 0.2 === 0.3 é falso, mas 0.5 + 0.25 === 0.75 é verdadeiro?
Porque 0.5, 0.25 e 0.75 são 2⁻¹, 2⁻² e 2⁻¹ + 2⁻², todos exatamente representáveis, e a soma deles não precisa de arredondamento. 0.1, 0.2 e 0.3 são armazenados de forma aproximada, e arredondar a soma dos dois primeiros cai um ULP acima do double para o qual 0.3 mapeia.
Isso acontece em toda linguagem de programação?
Toda linguagem construída sobre ponto flutuante binário IEEE 754 mostra o mesmo comportamento: JavaScript, Python, Java, C, C++, Go, Rust, Swift e o FLOAT do SQL. O que muda é a precisão de impressão padrão e se um tipo decimal exato vem na caixa, como o decimal do C# ou o módulo decimal do Python.