Ponto Flutuante
computacao-cientifica, algoritmos, seguranca analise-numerica basicoPonto Flutuante
Todo programador esbarra em 0.1 + 0.2 == 0.30000000000000004 e conclui que ponto flutuante é "impreciso". A descrição correta é outra: ponto flutuante é exato dentro de um conjunto finito de valores, e $0{,}1$ não pertence a esse conjunto.
Representação IEEE 754
Um número é armazenado como
$$(-1)^s \times 1{,}m \times 2^{e}$$
| Tipo | Bits | Sinal | Expoente | Mantissa | Dígitos decimais |
|---|---|---|---|---|---|
float32 | 32 | 1 | 8 | 23 | ~7 |
float64 | 64 | 1 | 11 | 52 | ~16 |
A mantissa tem um bit implícito (o 1 antes da vírgula), o que dá um dígito binário de graça.
O ponto central: a base é 2. Frações como $1/10$ são periódicas em binário, exatamente como $1/3$ é periódica em decimal. Por isso $0{,}1$ não tem representação exata, e a soma acumula o erro dos dois arredondamentos.
Valores que são exatos: inteiros até $2^{53}$ em float64, e frações cujo denominador é potência de 2 ($0{,}5$, $0{,}25$, $0{,}125$).
Epsilon de máquina
$$\epsilon = 2^{-52} \approx 2{,}22\times10^{-16} \quad (\text{float64})$$
É o menor $\epsilon$ com $1 + \epsilon \ne 1$. Toda operação aritmética tem erro relativo limitado por $\epsilon/2$.
Consequência prática: espere cerca de 16 dígitos decimais significativos, não 16 casas decimais. A precisão é relativa, então $10^{20}$ tem incerteza da ordem de $10^{4}$.
Valores especiais
| Valor | Origem |
|---|---|
+0.0, -0.0 | Zeros com sinal; iguais na comparação |
inf, -inf | Estouro, divisão de não zero por zero |
nan | $0/0$, $\infty-\infty$, raiz de negativo |
nan tem uma propriedade que surpreende: nan != nan. Isso é deliberado e é o teste padrão para detectá-lo (x != x). Como consequência, um nan num vetor contamina qualquer ordenação ou comparação, frequentemente de forma silenciosa.
inf costuma ser mais útil que uma exceção: ele se propaga, e permite continuar o cálculo e verificar no fim.
Cancelamento catastrófico
O erro mais perigoso. Ao subtrair dois números próximos, os dígitos significativos se cancelam e o erro relativo explode.
A fórmula de Bhaskara é o exemplo clássico. Com $b^2 \gg 4ac$, a raiz $\frac{-b + \sqrt{b^2-4ac}}{2a}$ subtrai dois números quase iguais. A correção é usar a forma racionalizada para essa raiz:
import math
def raizes(a, b, c):
d = math.sqrt(b*b - 4*a*c)
# escolhe o sinal que evita cancelamento
q = -(b + math.copysign(d, b)) / 2
return q / a, c / q
O mesmo fenômeno explica expm1 e log1p: calcular $e^x - 1$ diretamente para $x$ pequeno cancela tudo.
Regra prática: desconfie de toda subtração entre valores próximos. Reformule algebricamente para evitá-la.
Soma e associatividade
A adição em ponto flutuante não é associativa:
>>> (1e16 + 1) - 1e16
0.0
>>> 1e16 + (1 - 1e16)
0.0
>>> (1e16 - 1e16) + 1
1.0
Isso tem consequências reais em paralelismo: somar um vetor com $N$ threads dá resultado diferente de somar sequencialmente, e diferente a cada execução se a ordem de redução variar. É a causa mais comum de "resultados não reproduzíveis" em código numérico paralelo.
Para somar muitos valores com precisão, use o algoritmo de Kahan, que mantém um termo de compensação:
def soma_kahan(valores):
s = 0.0
c = 0.0
for x in valores:
y = x - c
t = s + y
c = (t - s) - y # recupera o que se perdeu
s = t
return s
math.fsum do Python faz algo ainda melhor: soma exata com arredondamento único.
Comparação correta
Nunca compare floats com ==. As alternativas, em ordem de preferência:
import math
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0) # padrão recomendado
import numpy as np
np.allclose(A, B, rtol=1e-5, atol=1e-8) # para vetores
A tolerância relativa é o que faz sentido na maioria dos casos; a absoluta só é necessária perto de zero, onde a relativa perde significado. Usar só tolerância absoluta é um erro comum: abs(a-b) < 1e-9 é razoável para valores da ordem de 1 e absurdamente restritivo para valores da ordem de $10^{10}$.
Quando não usar ponto flutuante
Dinheiro. Use inteiros em centavos ou tipo decimal. Um sistema financeiro que soma valores em float acumula centavos de diferença que aparecem na conciliação.
Contadores e índices. Inteiros.
Comparações exatas. Se a igualdade precisa ser exata, o tipo está errado.
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') # Decimal('0.3') — exatoExemplo trabalhado
Por que 0.1 + 0.2 != 0.3?
Em float64, $0{,}1$ é armazenado como aproximadamente $0{,}1000000000000000055511\ldots$ e $0{,}2$ como $0{,}2000000000000000111022\ldots$. A soma exata dessas aproximações arredonda para o float mais próximo, que é $0{,}3000000000000000444089\ldots$ — enquanto o float mais próximo de $0{,}3$ é $0{,}2999999999999999888978\ldots$.
Dois valores diferentes, ambos "corretos" dentro do sistema. O erro não está na máquina; está na expectativa.
Erros comuns
- Comparar com
==. - Somar valores de magnitudes muito diferentes sem compensação.
- Usar
floatpara dinheiro. - Ignorar
nane propagá-lo silenciosamente por todo o cálculo. - Supor associatividade e esperar reprodutibilidade em código paralelo.
- Usar tolerância absoluta em valores de grande magnitude.
Leituras recomendadas
- Goldberg, "What Every Computer Scientist Should Know About Floating-Point Arithmetic" — leitura obrigatória, gratuita e ainda insuperável.
- Higham, Accuracy and Stability of Numerical Algorithms — o tratado completo sobre erro numérico.
- O padrão IEEE 754 e a documentação de
math.fsume do módulodecimaldo Python. - Site "Floating Point Math" (0.30000000000000004.com) — demonstração do fenômeno em dezenas de linguagens.
David Goldberg (1991). What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys 23(1), 5-48. DOI: 10.1145/103162.103163. Nicholas J. Higham (2002). Accuracy and Stability of Numerical Algorithms. SIAM. DOI: 10.1137/1.9780898718027.
Referências
- David Goldberg (1991). What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys 23(1), 5-48.
- Nicholas J. Higham (2002). Accuracy and Stability of Numerical Algorithms. SIAM.