Números em ponto flutuante e seus problemas
Contents
28. Números em ponto flutuante e seus problemas#
Este capítulo é um compêndio sobre a representação numérica em computadores, sistema de ponto flutuante, bem como notas sobre causas de erros. O texto é essencialmente uma reprodução adaptada do conteúdo encontrado no site floating-point-gui.de.
28.1. Perguntas e respostas#
28.1.1. Por que a soma 0.1 + 0.2
não é exatamente 0.3, mas resulta em 0.30000000000000004
?#
Porque, internamente, os computadores usam um formato de ponto flutuante binário limitado que não pode representar com precisão números como 0.1, 0.2 ou 0.3. Quando o código é compilado ou interpretado, 0.1
já é arredondado para o número mais próximo consoante este formato, o que resulta em um pequeno erro de arredondamento antes mesmo de o cálculo acontecer.
# ?!
0.1 + 0.2
0.30000000000000004
28.1.2. Este sistema parece um tanto estúpido. Por que os computadores o utilizam para fazer cálculos?#
Não se trata de estupidez. O sistema é apenas diferente. Números decimais não podem representar com precisão qualquer número, a exemplo da fração 1/3
. Então, temos que usar algum tipo de arredondamento para algo como 0.33
. Afinal, não podemos esperar que a soma 0.33 + 0.33 + 0.33
seja exatamente igual a 1
, não é mesmo?
Os computadores usam números binários porque são mais rápidos ao lidar com eles e, para a maioria dos cálculos usuais, um pequeno erro na 17a. casa decimal não é relevante, pois, de qualquer maneira, os números com os quais trabalhamos não são “redondos” (ou exatamente “precisos”).
28.1.3. O que pode ser feito para evitar este problema?#
Isso depende do tipo de cálculo pretendido.
Se a precisão dos resultados for absoluta, especialmente quando se trabalha com cálculos monetários, é melhor usar um tipo de dado especial, a saber o decimal
. Em Python, por exemplo, há um módulo de mesmo nome para esta finalidade específica. Por exemplo:
# 0.1 + 0.2
from decimal import Decimal
a,b = Decimal('0.1'), Decimal('0.2')
print(a+b)
0.3
Se apenas se deseja enxergar algumas casas decimais, pode-se formatar o resultado para um número fixo de casas decimais que será exibido de forma arredondada.
# imprime resultado como float
print(0.1 + 0.2)
# imprime resultado com 3 casas decimais
print(f'{0.1 + 0.2:.3f}')
0.30000000000000004
0.300
28.1.4. Por que outros cálculos como 0.1 + 0.4
funcionam corretamente?#
Nesse caso, o resultado, 0.5
, pode ser representado exatamente como um número de ponto flutuante e é possível que haja erro de cancelamento nos números de entrada. Porém, pode não podemos confiar interamente nisto. Por exemplo, quando tais dois números são primeiro armazenados em representações de ponto flutuante de tamanhos diferentes, os erros de arredondamento podem não compensar um com o outro.
Em outros casos, como 0.1 + 0.3
, o resultado não é realmente 0.4
, mas próximo o suficiente para que 0.4
seja o menor número mais próximo do resultado do que qualquer outro número em ponto flutuante. Muitas linguagens de programação exibem esse número em vez de converter o resultado real de volta para a fração decimal mais próxima. Por exemplo:
print(0.1 + 0.4)
print(0.1 + 0.1 + 0.1 - 0.3) # 0 !
0.5
5.551115123125783e-17
28.2. Comparação de números em ponto flutuante#
Devido a erros de arredondamento, a representação da maioria dos números em ponto flutuante torna-se imprecisa. Enquanto essa imprecisão permanecer pequena, ela poderá ser geralmente ignorada. No entanto, às vezes, os números que esperamos ser iguais (por exemplo, ao calcular o mesmo resultado por diferentes métodos corretos) diferem ligeiramente de tal forma que um mero teste de igualdade implica em falha. Por exemplo:
a = 0.15 + 0.15
b = 0.1 + 0.2
# Os resultados abaixo deveriam ser verdadeiros (True),
# mas não o são!
print(a == b)
print(a >= b)
False
False
28.2.1. Margens de erro: absoluto x relativo#
Ao compararmos dois números reais, o melhor caminho a seguir não é verificar se os números são exatamente iguais, mas se a diferença entre ambos é muito pequena. A margem de erro com a qual a diferença é comparada costuma ser chamada de “epsilon”. A forma mais simples seria por meio do erro absoluto. Por exemplo:
# comparação por erro absoluto
if abs(a - b) < 1e-5:
print('OK!')
OK!
Isto é, a expressão acima é matematicamente equivalente a \(|a - b| < \epsilon\) para \(\epsilon=10^{-5}\).
Entretanto, essa é uma maneira ruim de comparar números reais, porque um epsilon fixo escolhido que parece “pequeno” pode, na verdade, ser muito grande quando os números comparados também forem muito pequenos. A comparação retornaria verdadeiro para números bastante diferentes. E quando os números são muito grandes, o epsilon pode acabar sendo menor que o menor erro de arredondamento, de forma que a comparação sempre retorna falso.
Assim, é razoável verificar se o erro relativo é menor que o epsilon.
# comparação por erro relativo
if abs((a - b)/b) < 1e-5:
print('OK!')
OK!
Mas, esta forma ainda não é inteiramente correta para alguns casos especiais, a saber:
quando tanto
a
quantob
são iguais a zero, a fração resultante0.0/0.0
é uma indefinição do tipo not a number (NaN
), a qual gera uma exceção em algumas plataformas, ou retorna falso para todas as comparações;
a,b = 0,0
abs((a-b)/b) # exceção 'ZeroDivisionError'
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-175-fb6d2c8ce8ce> in <module>
1 a,b = 0,0
----> 2 abs((a-b)/b) # exceção 'ZeroDivisionError'
ZeroDivisionError: division by zero
quando apenas
b
é igual a zero, a divisão produz o infinito (\(\infty\)), que pode gerar uma exceção ou ser maior do que epsilon mesmo quandoa
for menor;
a,b = 1e-1,0
abs((a-b)/b) # exceção 'ZeroDivisionError'
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-176-4bc2e683f9ff> in <module>
1 a,b = 1e-1,0
----> 2 abs((a-b)/b) # exceção 'ZeroDivisionError'
ZeroDivisionError: float division by zero
quando
a
eb
são muito pequenos, mas estão em lados opostos a zero, a comparação retorna falso, ainda que ambos sejam os menores números diferentes de zero possíveis.
a,b = -0.009e-20,1.2e-19
abs((a-b)/b) < 1e-5
False
Além disso, o resultado pode não ser sempre comutativo. Isto é, abs((a-b)/b)
pode ser diferente de abs((b-a)/b)
.
É possível escrever uma função – veja abaixo – capaz de passar em vários testes para casos especiais, porém ela usa uma lógica com pouca obviedade. A margem de erro deve ser definida de maneira diferente dependendo dos valores de a
ou b
, porque a definição clássica de erro relativo torna-se insignificante nesses casos.
def compare_float(a:float,b:float,eps:float) -> bool: # ":" e "->" são apenas anotações didáticas
import sys
MIN = sys.float_info.min # menor float : ~ 1.80e+308
MAX = sys.float_info.max # maior float : ~ -2.23e+308
diff = abs(a-b)
a = abs(a)
b = abs(b)
if (a == b): # trata 'inf'
return True
elif (a == 0 or b == 0 or (a + b < MIN)): # a ou b = 0 ou ambos extremamente próximos de 0
return diff < (eps*MIN)
else: # erro relativo
return diff/(min(a + b, MAX)) < eps
Exemplos:
print(compare_float(1e-3,1e-3,1e-10))
print(compare_float(1e-3,1.1e-3,0.01))
print(compare_float(10.111,10.1111,1e-5))
print(compare_float(10.111,10.1111,1e-6))
True
False
True
False
28.2.2. Referências para estudo#
28.2.2.1. Miscelânea#
What Every Computer Scientist Should Know About Floating-Point Arithmetic, artigo publicado por David Goldberg na ACM Computing Surveys, Vol. 23 (1), 1991. DOI: 10.1145/103162.103163
William Kahan’s Homepage (arquiteto do padrão IEEE 754, e vários outros links)
28.2.2.2. Visualizadores interativos de números no padrão IEEE 754#
28.2.2.3. Exemplos de comparação de números em ponto flutuante#
28.2.2.4. Livros#
Modern Computer Architecture and Organization, por Jim Ledin
Computer Organization and Architecture Designing for Performance, por William Stallings
Numerical Computing with IEEE Floating Point Arithmetic, por Michael L. Overton