Ponto Flutuante

computacao-cientifica, algoritmos, seguranca analise-numerica basico

Ponto 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}$$

TipoBitsSinalExpoenteMantissaDígitos decimais
float32321823~7
float646411152~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

ValorOrigem
+0.0, -0.0Zeros com sinal; iguais na comparação
inf, -infEstouro, 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') — exato

Exemplo 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 float para dinheiro.
  • Ignorar nan e 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.fsum e do módulo decimal do 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

  1. David Goldberg (1991). What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys 23(1), 5-48.
  2. Nicholas J. Higham (2002). Accuracy and Stability of Numerical Algorithms. SIAM.