Mais problemas com ponto-flutuante

Já falei por aqui que lidar com comparações e aritimética com ponto-flutuante é difícil. Para entender isso, basta considerar que:

  • Ponto-flutuante é, nada mais, nada menos, do que a representação em “notação científica” de um número real. Mas essa notação é representada, no fim das contas, em base 2;
  • Alguns valores, em base decimal, não podem ser precisamente representados em base 2. O valor “0.1” é um exemplo disso*;
  • Além dos valores reais, o resultado de uma operação em ponto-flutuante pode ser um “não-número”, “+infinito” ou “-infinito”;
  • O valor zero pode ser positivo ou negativo! Isso não é muito problemático, mas alguns “macetes” podem falhar;
  • Devido à limitação de espaço, a diferença entre dois números em ponto-flutuante adjacentes pode ser diferente, para faixas diferentes**.

A segunda afirmação é óbvia: Assim como não é possível representar alguns valores em base 3 na base 10… Por exemplo, converter 0.1(3) para base 10 gera uma dízima periódica (0.33333…). Essa afirmação também tem um aspecto interessante: Como todo número em ponto-flutuante tem precisão restrita (por causa do tamanho dos tipos floatdoublelong double), o número máximo de valores represntáveis é facilmente calculável… Já que um float tem 32 bits de tamanho, em teoria podemos representar, no máximo, 4 bilhões de valores diferentes. Esse valor diminui um pouco se considerarmos outras idiossincrasias da especificação IEEE 754.

A última afirmação vem do fato de que a diferença entre dois valores adjacentes, em ponto-flutuante, pode variar dependendo da magnitude do valor. Eis a explicação: Suponha que possamos representar um valor decimal com precisão de uma “casa” decimal apenas, multiplicado por 10 elevado a um expoente inteiro:

número = 0.d * 10^n

Se ‘n’ for zero a diferença entre os dois valores adjacentes representáveis é de exatamente 0.1, mas se n for negativo, essa diferença diminui à medida que ‘n’ diminui. Da mesma forma que a diferença entre dois valores adjacentes, para ‘n’ maior que zero, aumenta, diminuindo a precisão do número final.

A sabedoria “popular” nos diz que comparações entre valores em ponto-flutuante devem ser feitas através de um valor ε (epsilon). Essa letra grega deveria representar a menor diferença entre dois valores em ponto-flutuante adjacentes. Como vimos acima, isso nem sempre é verdadeiro!!! Eis a macro bastante usada:

#define COMPARE_FLOATS(x,y) (fabs((x) - (y)) < FLT_EPSILON)

Essa comparação (chamada de comparação “absoluta”) não é lá das melhores para comparações com valores entre 0.0f e 1.0f, mas é boa com valores grandes. Uma outra técnica (chamada ULP, de “Units in the Last Place”) que pode ser usada é comparar a própria representação “inteira” do ponto-flutuante:

#include <math.h>
#include <stdlib.h>

typedef union {
  float f;
  unsigned int i;
  struct {
    unsigned int mantissa:23;
    unsigned int expoent:8;
    unsigned int sign:1;
  } s;
} f32_t;

int compare_floats2(float x, float y)
{
  /* Verifica o sinal */
  if (((f32_t *)&x)->s.sign != ((f32_t *)&y)->s.sign)
  {
    /* Cerca a possibilidade de compara +0 com -0. */
    if (x == y)
      return 1;
    return 0;
  }

  return abs(((f32_t *)&x)->i - ((f32_t *)&y)->i) < 2;
}

Estou considerando uma ULP de apenas 1 unidade na rotina acima. Se a diferença for menor que 2 unidades, considero os valores “quase” iguais. A rotina funciona para valores pequenos. Mas isso também implica em outros problemas: Operações em ponto-flutuante sofrem do problema do “arredondamento”. É uma técnica conhecida em notação científica: O digito menos significativo é o mais impreciso e, em certos casos, pode ser desconsiderado, para efeitos de comparação. De fato, existem diferenças de métodos de arredondamento de constantes de compilador para compilador (o GCC é um pouco diferente do Visual C++, por exemplo).

O problema do arredondamento é relativamente fácil de acertar: Basta usar ULPs maiores… Ao invés da comparação da diferença com 2 ULPs, podemos aumentar para 3 ou mais, por exemplo. Outro problema sério com ULPs é que valores em ponto-flutuante “grandes” podem ser muito parecidos (e, por isso, a técnica deve ser usada apenas para valores pequenos!). A chamada abaixo retorna TRUE:

convert_floats2(67329.234, 67329.242);

A diferença é de apenas 0.08! E 0.08 é bem maior que FLT_EPSILON, não é? Poderíamos mesclar as duas técnicas… Uma possibilidade seria fazer uma comparação absoluta (com COMPARE_FLOATS) e depois uma via ULPs.

Deixei de fora a comparação “relativa”. A idéia é usar uma “porcentagem” do maior valor da comparação como se fosse o ε. Ela é mais ou menos assim:

#define COMPARE_FLOATS_REL(x,y) ((fabs((x) - (y)) <= (max(fabs((x)), fabs((y))) * FLT_EPSILON))

Por que usar max() e não min()? Por que um dos valores pode ser 0.0f! Usar a comparação relativa é parecido com usar ULPs, sem uma precisão tão boa… Note que nem ULPs, nem comparações absolutas ou relativas são panacéias. Devem ser escolhidas e testadas de acordo com o contexto.

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