O problema do ponto-flutuante

MaRZ acabou de postar a curiosidade do 0.9999… De fato, essa curiosidade não é perigosa apenas na matemática convencional, os números em ponto-flutuante apresentam outras “curiosidades” perigosas também.

É bem sabido que os tipos integrais (char e int, e suas variações) são exatos. Ou seja, 1 é 1, 2 é 2, e assim por diante. Mas, os tipos integrais não são precisos. Se você fizer uma divisão de 3 por 2 obterá 1 (e não 1.5, por motivos óbvios). Para resolver isso existem os tipos em ponto-flutuante. Existem 3: float, double e long double. Outros compiladores e arquiteturas possuem tipos adicionais como half (é o caso das GPUs, nas placas de vídeo).

Para efeitos de análise, veremos apenas o tipo float aqui, mas o que direi também vale para os demais tipos.

Acontece que float tem um tamanho fixo de 32 bits (double tem 64, long double tem 80 e half, quando existe, tem 16). E o valor “quebrado” é armazenado em forma binária (usando a base numérica 2). A fórmula geral para expressar um número em ponto-flutuante é:

  valor = m·2e-127

Onde, ‘m’ é um valor entre 0 (zero) e ±1 (um, positivo ou negativo) – obedecendo à condição de: 0 ≤ |m| < 1 — o valor ‘e’ é o expoente da base 2 (é um fator que “desloca a virgula” do número ‘m’) – obedecendo à condição de: 0 < e ≤ 255 . Dê uma olhada neste artigo do wikipedia sobre single precision floating point.

Muito bem: Variáveis de ponto-flutuante ajudam a aumentar a precisão nos cálculos, mas o custo disso é a exatidão! Como o tipo float tem um conjunto limitado de bits para armazenar ‘m’, certos números simplesmente não podem ser representados. Além da limitação dos número de bits, temos a “cursiosidade” de que certos números são representáveis em uma base (a base 3, por exemplo) e o mesmo número não pode ser representado em outra (na base 10, por exemplo). É o caso de ⅓, por exemplo. Em base 3 esse número é 0.1 (ou 3-1), mas na base 10 temos 0.3333333…

No caso da base 2, só para citar um exemplo alarmante, os seguintes números não podem ser representados em ‘m’ com exatidão: 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8 e 0.9.

O que o compilador (e o coprocessador matemático dentro da sua CPU) faz é “interpretar” certos valores como se fossem exatos, fazendo algumas mágicas exotéricas do pico do himalaia (ou seja, sei lá o que eles fazem!). E onde isso nos deixa?

O código abaixo é bastante comum em qualquer linguagem (x e y são floats aqui):

  if (x == y)
    DoSomething();

Neste caso é MUITO provável que DoSomething() jamais seja executado, dependendo de como x e y (mesmo que pareçam iguais) sejam calculados. Para ponto-flutuante temos que trabalhar com limites, não com igualdades (e também não com “desigualdades”, pelo mesmo motivo). O código correto é este aqui:

  if (fabs(x - y) <= EPSILON)
    DoSomething();

Onde o valor de EPSILON deve ser pequeno o suficiente para garantir a precisão desejada mas também deve ser maior que a constante FLT_EPSILON (que é o menor valor possível para um float).

Outros testes:

  f = x - y;
  if (fabs(f) <= EPSILON) DoSomething();  /* testa igualdade. */
  if (f > EPSILON) DoSomething(); /* Testa se x > y */

O resto é fácil deduzir, né?

Então, não tomem cuidado apenas com 0.9999…, mas com todo e qualquer cálculo envolvendo variáveis em ponto-flutuante!

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s