Por que 0.4 - 0.5 = 0.09999999999999998 em Javascript?
Ontem o Marco Gomes perguntou por que 0.4 - 0.5 é 0.09999999999999998 em vez de 0.1 em Javascript. Esse post é uma tentativa de resposta à essa pergunta.
Existe um número infinito de números reais entre 0.0 e 1.0 então não dá pra representar todos os números reais com precisão infinita já que a memória do computador é finita. Ao contrário do que disseram nos comentários, dá sim pra representar 0.4, 0.5 e 0.1 em 64 bits (tamanho do float no Javascript) e em até muito menos que 64 bits o que garante mais 9’s na aproximação de 0.1, mas não está livre dos mesmos problemas.
Vou usar representação de 32 bits para todos os exemplos abaixo para não ter que lidar com números muito longos. O Javascript usa 64 bits.
Quando você digita “0.4” solicitando que o computador represente o número real 0.4 em um número finito de bits no padrão IEEE 754 (padrão usado por CPUs e linguagens modernas) o computador decide codificar 0.4 como \(2^{-2} \times 1.600000023841858\) (é o que cabe em 32 bits seguindo as regras do IEEE 754).
Essa representação no formato \(2^{expoente} \times mantissa\) é chamada de ponto flutuante e o padrão IEEE 754 define como esses números são transformados em operações.
Se você usar uma calculadora de alta precisão vai ver que
\[2^{-2} \times 1.600000023841858 = \\0.4000000059604645\]Se você pedir para imprimir esse número de novo verá “0.4” porque a rotina que converte pontos flutuantes de 32 bits para texto seguindo as regras do IEEE 754 decide que “0.4” é o número mais próximo daquilo que está tentando-se representar com expoente igual a -2 e mantissa igual a 1.600000023841858. Mesmo assim, dá pra dizer que existe um erro de 0.0000000059604645 nessa representação.
Representar “0.5” é muito menos problemático porque computadores são muito bons em dividir por 2 e 0.5 é 1 dividido por 2.
\[0.5 = 2^{-1} \times 1.0\]Fazendo a subtração usando as representações em ponto flutuante de 32 bits de 0.4 e 0.5 em uma calculadora de alta precisão:
\[0.4000000059604645 - 0.5 = \\-0.0999999940395355\]Esse resultado contém o erro que vem da representação do 0.4. Como a cada operação com floats IEEE 754, os números são arredondados para o máximo de precisão possível com um número finito de bits (32 nesses exemplos), então o resultado visível será -0.099999994.
Por que esse pequeno erro agora se tornou “visível”? Porque a precisão de pontos flutuante é variável! Dá pra representar -0.0999999940395355 com um pouco mais de precisão do que 0.4000000059604645 “movendo o ponto” e o IEEE 754 aproveita isso. -0.099999994 é representado como \(2^{-4} \times 1.5999999046325684\). Esse -4 é a “flutuação do ponto” e dá a possibilidade de retornar mais uma casa decimal com confiança. -0.099999994 é mais próximo de -0.1 do que -0.09999999, mas 0.400000005 (9 casas) é menos próximo de 0.4 do que 0.40000000 (8 casas).
Um exemplo de número que é muito amigável à representação de ponto flutuante é 0.0000000000009094947017729282379150390625. Ao contrário de 0.4 pode ser representado sem erro algum por \(2^{-40} \times 1.0\).
Em ponto flutuante decimal (em vez de binário comum em computadores) é muito fácil representar 0.0000000000000000000000000000000000000001 como \(10^{-40} \times 1.0\) mas é bem mais problemático de se representar com ponto flutuante binário:
\[2^{-126} \times 0.008507013320922852 = \\ 0.0000000000000000000000000000\\ 000000000000999994610111476009\\ 580469753702428423236444030435\\ 167589607897177528261778434170\\ 992113649845123291015625\]Representar números reais em computadores não é fácil e é necessário entender as limitações de cada representação (pontos flutuantes, ponto fixo, frações, fórmulas simbólicas…) para minimizar problemas em aplicações práticas.