Ainda uma outra interpretação sobre ponto flutuante

Essa eu vi no material sobre análise dos jogos Wolfenstein 3D e DOOM (muito bom, e pode ser baixado aqui):

Já mostrei, diversas vezes, a estrutura de um float por aqui, bem como a equação que fornece o conteúdo, em decimal, correspondente a essa estrutura:

Estrutura de um float

\displaystyle v=\left(-1 \right )^s\cdot\left(1+\frac{f}{2^{23}} \right )\cdot2^{e-127}

Claro que essa equação aplica-se apenas aos valores normalizados, mas os valores subnormais (quando e=0) não são muito diferentes:

\displaystyle v=\left(-1\right)^s\cdot\frac{f}{2^{23}}\cdot2^{-126}

Yep… o expoente não é -127, mas -126, para os subnormais.

O que eu chamei de “fração”, aí em cima, é um valor inteiro de 23 bits, correspondente ao numerador f. O texto dos referidos livros dão uma interpretação muito interessante: Note que temos 3 partes na estrutura:

\underbrace{\left(-1 \right )^s\vphantom{\left(\frac{f}{2^1}\right)}}_{sinal}\cdot\underbrace{\left(1+\frac{f}{2^{23}}\right)}_{mantissa}\cdot\underbrace{2^E\vphantom{\left(\frac{f}{2^1}\right)}}_{escala}

Usei o term “mantissa” aqui porque a droga do latex não aceita caracteres acentuados e estou com preguiça em buscar a sequência correta para conseguí-los (yep! preguiça mesmo!). O correto seria “fração”… Mas, o interessante é que essa fração (F=\left(1+\frac{f}{2^{23}}\right)) sembre está da faixa 1\leqslant F<2. Isso significa que quem dá o ponto inicial do valor representado é a escala.

Por exemplo, o valor 1.5 tem escala 2^0, porque a mantissa vai de 1.0 até quase 2.0. Já o valor 130 tem escala 2^7 (128), porque o valor pode ir de 128 até quase 256. Dito de maneira geral, uma fração representa valores de 2^E até quase 2^{E+1}… É como se a escala fosse uma “janela” e a fração um “offset”. Abaixo temos um exemplo para o valor 1.5:

Assim, para conseguirmos o valor 3.14, por exemplo, basta achar a escala (para encontrarmos o início da faixa), ou seja:

\displaystyle E=\lfloor\log_2{N}\rfloor=\lfloor\log_2{3.14}\rfloor=1

Isso nos dá a faxa de [2^1,2^2) ou entre 2 e 4, incluindo o 2. Agora podemos encontrar o valor 3.14 subtraindo o início da faixa e calculando a posição correspondente (a fração) dentro da faixa – Isso nos dará um valor percentual dentro da faixa:

\displaystyle n=\frac{3.14 - 2^1}{2^2-2^1}=\frac{3.14 - 2}{4 - 2}=\frac{1.14}{2}=0.57

Aqui, 3.14 - 2^1 é o offset absoluto do valor começando do início da faixa, mas precisamos dividir esse valor pela faixa inteira (2^2 - 2^1) para obter um valor relativo dentro da faixa… Multiplicando esse valor n pelo denominador da fração, para obter f, temos:

\displaystyle f = \lceil n\cdot2^{23}\rceil=\lceil0.57\cdot2^{23}\rceil=4781507

Os valores de e (minúsculo) e f, então, são: e=128 e f=4781507. Vamos confirmar isso:

/* test.c */
#include <stdio.h>

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

int main( void )
{
  float f = 3.14;
  struct float_T *p = (struct float_T *)&f;

  printf( "s=%u, e=%u, f=%u\n", p->s, p->e, p->f );
}

Compilando e executando:

$ cc -o test test.c
$ ./test
s=0, e=128, f=4781507

Isso funciona para qualquer valor representável em ponto flutuante, até mesmo para valores subnormais, embora a “janela”, neste caso, seja fixa entre [0,1), o que facilita o cálculo, já que o expoente é fixo E=-126, para float.

Sobre a imprecisão:

Agora fica simples entender o conceito de ULP (Unit in the Last Position) com os conceitos de janela e faixa de abrangência. A fração \frac{f}{2^{23}} tem que ser escalonada por 2^E e, portanto, cada unidade de f corresponde a um degrau dentro da faixa e cada um desses degraus é 1 ULP (\frac{1}{2^{23}}\cdot2^E=2^{E-23}).

Essa é a imprecisão do valor representado… Repare que quanto maior E, maior o ULP…

Outro exemplo: 123456789

Na sequência:

  1. Otemos E: E=\lfloor\log_2{123456789}\rfloor=26;
  2. Temos então uma faixa entre: [2^{26},2^{27});
  3. Obtemos a posição relativa do valor na faixa: n=\frac{123456789-2^{26}}{2^{27}-2^{26}}=\frac{56347925}{67108864}
  4. Multiplicamos a posição relativa por 2^{23} e obtemos f: f=\lceil n\cdot2^{23}\rceil=7043491

Verificando:

\displaystyle v=\left(-1 \right )^0\cdot\left(1+\frac{7043491}{2^{23}} \right )\cdot2^{26}=123456792

Ficou próximo, né? Repare que o ULP é de 2^{E-23}=8. Ou seja, se o início de nossa janela é 2^{26}=67108864 e os degraus vão de 8 em 8, o valor 123456789 não pode ser representado exatamente!