Ponto flutuante não é uma panacéia para aritmética integral com valores muito grandes…

Já falei de valores pequenos aqui… coisas como valores de 0,1 não poderem ser representados exatamente em ponto flutuante devido ao problema da conversão de base numérica (decimal → binário), mas resta outro problema que pode passar desapercebido: Números muito grandes também não podem ser exatamente representados!

Considere o tipo float, que tem 24 bits significativos… Qualquer valor inteiro com mais de 24 bits terá os bits inferiores arredondados. É o caso, por exemplo, do valor 16777217 (0x1000001) que tem 25 bits de tamanho:

float f = 16777217;

printf("%.8g\n", f);

Esse fragmento de código imprimirá 16777216. Para entender porque precisamos da representação binária inteira do valor com os 24 bits superiores separados: 0b\underbrace{100000000000000000000000}_{24\,bits}1. Como o valor é inteiro, o arredondamento mais próximo é extirpar o bit inferior excedente.

A mesma coisa pode ser observada com valores realmente grandes, como 20000001325478. Esse valor será codificado como 20000001753088, e é maior que o valor original por causa do arredondamento. Abaixo o caso de teste, se você estiver interessado:

long long x = 20000001325478LL;
float f = x;

printf("%#llx -> %.15g\n", x, f);

O valor de x será 0x12309cf979a6, que tem 45 bits de tamanho, se pegarmos apenas os 25 últimos obteremos 0x12309cf, que será arredondao para 0x12309d0 e, obtendo apenas os 24 bits superiores teremos 0x9184e8. Esses são os bits signifiativos que aparecerão no float, lembrando que o MSB é implícito. Observe agora o fator de escalonamento:

struct float_s {
  unsigned int f:23;
  unsigned int e:8;
  unsigned int s:1;
};

struct float_s *p = (struct float_s *)&f;

printf("f:%#x, e:%d, s:%d\n", p->f, (int)p->e - 127, p->s);

Isso nos dá um expoente E de 44, ou seja, deve-se deslocar o ponto “flutuante” em 44 posições para a direita (shift dos bits significativos em 44-23=21 bits para a direita). Assim, o valor final é 0x12309d000000! A diferença deste valor para o original é exatamente de 427610…

Mas, o leitor deve estar pensando: Ambos os valores têm bem mais que 24 bits! Não se esqueça que os bits significativos são escalonados por um fator 2^E, ou seja, ele sofre shifts para a esquerda ou direita, de acordo com E. Este é o motivo pelo truncamento à direita, ao invés do à esquerda, como acontece com os tipos integrais (char, short, int, long int e long long int).

Outra coisa: O leitor pode estar pensando que a resolução desse problema está na adoção de um tipo com mais precisão, como double, por exemplo… Acontece que double usa 53 bits significativos. Qualquer valor integral com mais de 53 bits sofrerá o mesmo problema… Neste caso, a possível solução, com base no fato de que os processadores modernos suportam a manipulação de valores integrais com 64 bits, facilmente, é usar o tipo long double, que usa 64 bits significativos. É uma solução… mas, vale lembrar que usar o tipo long double implica em usar a pilha e as instruções do coprocessador matemático embutido em seu processador (80×87) e ele é bem mais lento que os métodos mais “modernos”, como SSE e AVX, que só suportam até o tipo double.

Devo lembrá-lo, também, que as especificações da linguagem C modernas (ISO 9899:1999 em diante) admitem um tipo inteiro de 128 bits de tamanho (__int128) e, assim, se esse for o caso, a solução acima cai por terra.

O ponto aqui é, fazendo uma comparação com o artigo anterior, que converter um valor com maior precisão (inteiro) para um de menor (os bits significativos em ponto flutuante) implicará num “truncamento” dos bits à direita (os menos significativos), diferente do que acontece quando fazemos o mesmo entre tipos integrais (que tendem a truncar à esquerda, ou aos MSBs)…

Anúncios