0.1 + 0.7 = 0.799999… ? Qual é a causa da imprecisão decimal em linguagens de programação?
Todos os estudantes de Javascript já passaram ou irão passar por por situações como essa:
Para sermos justos, não só em Javascript, mas em outras linguagens como por exemplo, Python e Ruby, este comportamento também está presente.
Mas o quê exatamente causa esta imprecisão quando trabalhamos com casas decimais?
Antes de começarmos, certifique-se que já sabe como converter números inteiros e fracionários para binários.
Se não souber, sem problemas! Criei estes materiais para te ajudar: https://webadventures.medium.com/como-converter-n%C3%BAmeros-fracion%C3%A1rios-em-bin%C3%A1rios-f045305ad65c
https://webadventures.medium.com/como-converter-inteiros-em-bin%C3%A1rios-e-vice-versa-5cb2250c85a7
Agora sem mais enrolação, mãos à obra!
O sistema binário e números inteiros
Como você já sabe, computadores entendem apenas binários. Todo tipo de dado que o computador recebe ou retorna, é convertido em números binários para poderem ser processados.
O sistema binário é preciso em representar números inteiros. E para os primeiros computadores, apenas números inteiros já eram o suficiente para as tarefas que eram programados para realizar.
Para exemplificar, vamos converter alguns inteiros em decimais:
- 33 = 2⁵(1) 2⁴(0) 2³(0) 2²(0) 2¹(0) 2⁰(1) = 1 0 0 0 0 1
- 25 = 2⁴(1) 2³(1) 2²(0) 2¹(0) 2⁰(1) = 1 1 0 0 1
- 12 = 2³(1) 2²(1) 2¹(0) 2⁰ (0) = 1 1 0 0
Precisão total!
Jogos em 8 ou 16 bits podiam ser programados apenas utilizando números inteiros, e mesmo quando era necessário lidar com preços, ou salários, que continham casas decimais, técnicas eram aplicadas para se utilizar apenas inteiros, como por exemplo, multiplicar o valor por 100, realizar as operações necessárias e depois unir a parte inteira com a decimal, que eram calculadas separadamente.
O sistema binário e números com ponto flutuante
Porém tudo mudou com a ascensão da computação gráfica e animação em 3D, onde trabalhar com números com casas decimais passava a ser agora parte do cotidiano. O problema começa aqui…
Quando um computador recebe um número com partes decimais, ele também precisa convertê-lo em binário para realizar o processamento dos dados. Porém, a grande maioria dos números fracionários, se tornam binários infinitos quando convertidos.
Para demonstrar, vamos converter algumas frações para binário:
- 0.4 = 0.4 x 2 = 0.8(0) 0.8 x 2 = 1.6(1) 0.6 x 2 = 1.2(1)
0.2 x 2 = 0.4(0) 0.4 x 2 = 0.8(0) 0.8 x 2 = 1.6 (1) 0.6 x 2 = 1.2 (1)
0.2 x 2 = 0.4 (0) ... = 0.01100110…
- 0.22 = 0.22 x 2 = 0.44(0) 0.44 x 2 = 0.88(0) 0.88 x 2 = 1.76(1)
0.76 x 2 = 1.52(1) 0.52 x 2 = 1.04(1) 0.04 x 2 = 0.08(0)
0.08 x 2 = 0.16(0)… = 0.0011100…
Então, como o computador lida com esse problema?
Aproximação! Lembre-se das aulas de geometria na escola, quando aproximávamos o valor de PI para 3.14.
PI é igual a 3.14159265… e suas casas decimais seguiam inifitamente. Porém não temos papel infinito para anotar infinitas casas decimais. Logo, o que fazemos é aproximar seu valor para um número na qual possamos calcular, que neste caso, aproximaríamos para 3.14.
A mesma coisa com a fração 1/3, que é igual a 0.33333333… e utilizamos seu valor aproximado como 0.3. Sabemos que o valor exato de 1/3 não é 0.3, mas é a melhor aproximação possível que podemos fazer na base decimal.
O computador faz a mesma coisa com números fracionários. Como sua conversão resulta em um binário de tamanho infinito e o computador não possui memória infinita para armazená-los, as linguagens de programação se utilizam de técnicas para aproximar ao máximo o valor binário do valor real, porém o resultado nunca será exato.
Para quem tiver curiosidade, pesquise pela norma IEEE-754, que é o padrão técnico que muitas linguagens utilizam para aritmética de ponto flutuante (inclusive Javascript)
Explicando as imprecisões
0.1 + 0.2 = 0.030000000000000004
No momento em que 0.1 e 0.2 são convertidos em binários, ele deixam de ser exatamente 0.1 e 0.2, passando a ser representados como uma aproximação desses valores, já que eles geram um binário infinito no momento da conversão. Sendo assim, a soma destes valores aproximados não irão ser exatamente igual a 0.3, gerando a imprecisão que observamos.
0.1 + 0.2 === 0.3 resultando em falso
Semelhante ao primeiro caso, os valores serão convertidos em binários antes de serem processados. Sendo assim, eles não serão exatamente 0.1, 0.2 e 0.3, passando a ser um valor aproximado a isso. Logo, 0.3 em binário será ligeiramente diferente de 0.1 + 0.2, pois as aproximações geram imprecisões no resultado.
Para provar isso, vamos converter 0.3 para binário:
Agora, vamos converter o binário de volta para fração:
Viu só? O binário gerado pela conversão não é exatamente 0.3, pois é uma aproximação, que quando convertida novamente para fração, é possível notar a imprecisão que foi gerada.
O mesmo ocorrerá para 0.1 ou 0.2:
Observando desta forma, fica mais simples perceber o motivo de 0.1 + 0.2 ser diferente de 0.3.
É verdade que algumas linguagens tem maior cuidado em tratar estes casos e devolver realmente 0.1 + 0.2 = 0.3 como esperaríamos, mas tenha em mente que são tratamentos realizados por estas linguagens específicas. Por debaixo dos panos, o sistema binário sempre terá esta imprecisão ao converter frações para a base 2.
Concluindo…
O motivo das imprecisões em operações com números fracionários em algumas linguagens de programação se dá ao fato da limitação do próprio sistema binário em representar números com casas decimais com precisão.
Converter frações em binário, na grande maioria dos casos, resultará em um binário infinito, e para lidar com este problema, as linguagens de programação implementam soluções para trabalhar com um valor aproximado destes binários, pois computadores não possuem memória infinita para armazenar casas decimais infinitas.
Como aproximações não são o valor exato do número sendo representado, pequenas imprecisões podem ser notadas quando precisamos trabalhar com casas decimais.
E ficamos por aqui!
Espero que tenha conseguido entender este problema que a primeira vista parece ser uma bizarrice sem tamanhos. Mas como tudo na vida há explicação, agora você está por dentro de como funciona esta imprecisão.
O conversor que utilizei para criar os exemplos desta postagem foi o RapidTables: https://www.rapidtables.com/convert/number/binary-to-decimal.html
Muito obrigado por ter acompanhado até aqui, e até a próxima!