Você está na página 1de 10

Ponto Flutuante

Frederico Lamberti Pissarra


27 de abril de 2021

Resumo
A matemática por detrás de representações em ponto flutuante. Neste
texto mostro apenas representações normalizadas. Ao final do texto há
observações sobre representação sub normal e NaN s.

A estrutura de um ponto flutuante na memória


A estrutura binária de uma representação em ponto flutuante, de acordo
com o padrão IEEE 754, para as três precisões1 : float, double e long double,
segue o que vai no diagrama abaixo.

Figura 1: Ponto flutuante – pela IEEE 754

O bit S é isolado no topo da estrutura (msb) seguido dos bits do expoente do


fator de escala, E, e dos bits da parte fracionária, F . É fácil observar que floats
têm 32 bits de tamanho, doubles têm 64 bits e long doubles, por estranho que
pareça, 80 bits.
Os valores de E e F são sempre inteiros e sem sinal. No caso, o bit S dá o
sinal ao valor final, positivo se S = 0, negativo se S = 1. O sinal do "fator de
escala"será explicado adiante.
1 Na realidade as precisões são nomeadas "precisão simples", "precisão dupla"e "precisão

estendida", respectivamente.

1
A equação genérica que descreve o valor armaze-
nado
Dados os bits de S, E e F , a equação que descreve o valor armazenado, n,
em qualquer uma das precisões é:
 
S F
n = (−1) · 1 + p−1 · 2E−Ebias (1)
2
A constante p é a precisão total, em bits, do tipo. Isso significa que a
quantidade de bits de F é p − 1. Como visto na figura 1, para um float p = 24;
num double p = 53 e com um long double p = 64.
S
O primeiro termo da equação, (−1) , é o que dá o sinal a n. O segundo
termo, 1 + 2p−1 , também conhecido como mantissa, nos dá sempre um valor
F


entre 1 e 2, ou:
 
F
1 6 1 + p−1 < 2 (2)
2
Pode-se entender esse termo como uma sequência de bits que resulta no valor
fracionário 0b1.F . O terceiro termo, 2E−Ebias , é chamado de fator de escala.
Ele muda a escala do segundo termo para qualquer outro valor. Pode-se pensar
naquilo que vai armazenado na estrutura de um ponto flutuante como sendo
partições da faixa entre 1 e 2 escalonadas.
Já que todos esses valores contidos na estrutura são sempre positivo, consegue-
se um expoente negativo no fator de escala subtraíndo-o pela metade da faixa
do mesmo. Esse valor é outra constante chamada de "polarização"ou Ebias :

−Ebias 6 E − Ebias 6 Emax − Ebias


Aqui, as constante Emax e Ebias dependem do tamanho de E. Se E tem 8
bits (para um float) então, obviamente, Emax = 255 e Ebias será a metade da
faixa2 , 127. Abaixo temos as equações para os três tipos:
 
S F
nf loat = (−1) · 1 + 23 · 2E−127
2
 
S F
ndouble = (−1) · 1 + 52 · 2E−1023
2
 
S F
nlong double = (−1) · 1 + 63 · 2E−16383
2
Para facilitar algumas explicações vou escrever o expoente do fator de escala
com um e – não confundir com a constante de Euler – onde:

e = E − Ebias (3)
2 Com 8 bits temos o valor biário máximo de 0b11111111. Deslocando esses 8 bits para a

direita em 1 bit temos 0b01111111.

2
Também para tornar as equações menos complicadas, usarei a equação ge-
nérica abaixo, desconsiderando o termo do sinal:
 
F
n = 1 + p−1 · 2e (4)
2
Como já explicado, outra forma de entender a equação acima é observar
que o fator de escala, 2e , determina uma faixa para a fração. Isso pode ser
desenhado assim:

Figura 2: Início e fim da faixa.

Onde f = 1 + 2p−1 F
. Isso nos dá a posição dentro do intervalo dado pelo


fator de escala: [2e , 2e+1 ). Pode-se pensar na mantissa como um offset e o fator
de escala como a base da faixa escolhida, como se fosse um ponteiro associado
ao um índice, mas no domínio dos números racionais (Q).

Convertendo um valor decimal para a estrutura de


ponto flutuante
De acordo com a interpretação gráfica na figura 2, a primeira coisa a fazer é
encontrar a base da faixa e, portanto, e:

e = blog2 nc (5)
E como a mantissa segue a inequação 2, temos que:

2e 6 n < 2e+1
Já temos e e só precisamos achar F . Basta manipular a equação 4 e temos:
 
F
n ≈ 1 + p−1 · 2e
2
F ≈ n · 2−e − 1 · 2p−1 (6)


A aproximação está ai por questões de arredondamento, já que F precisa ser


um inteiro sem sinal. Mais sobre isso adiante.

3
Repare que o termo (n · 2−e − 1) sempre será um valor entre 0 e 1, desde
que desconsideremos a possibilidade de n ser zero3 . É bom reparar também
que a equação 6 precisa tanto de n quanto de e, que calculamos previamente na
equação 5.
Como exemplo, consideremos n = 1.25. Usando as equações 5 e 6 temos:

e = blog2 1.25c = 0
F ≈ 1.25 · 20 − 1 · 2p−1


Se considerarmos um tipo float, então p = 24, logo:

F ≈ 0.25 · 223 ≈ 2097152


Se substituirmos os e e F calculados acima na equação 4, temos:
 
2097152
nf loat = 1 + · 20 = 1.25
223
Quod Erat Demonstrandum.
No exemplo escolhi um valor que sei que pode ser exatamente representado.
Vamos a um que não pode: 0.1. Das equações 5 e 6, temos:

e = blog2 0.1c = −4
F ≈ 0.1 · 24 − 1 · 2p−1


Num float, F = 0.1 · 24 − 1 · 223 = 5033164.8 que, obviamente, será




arredondado para 5033165. O resultado é, pela equação 4:


 
5033165
n= 1+ · 2−4 = 0..100000001490116119384765625
223
Esse é o valor mais próximo de 0.1 que pode ser representado num float.
Mais sobre isso, adiante.

Se F é um inteiro, o que acontece se ele for incre-


mentado ou decrementado?
Para responder isso consideremos e = 0 e F = 0. Isso nos dará o valor
F = (1 + 0) · 20 = 1.0. Cada incremento de F , nessa situação nos dá um
pequeno distanciameto entre dois valores de n adjacentes de:
1
= = 21−p
2p−1
3 Para n = 0 existe outra consideração a ser feita, relativa aos valores sub normais, que

falarei mais adiante.

4
Isso quer dizer que se o valor n encontra-se na faixa entre [1, 2),  será o
incremento mínimo possível dentro dessa faixa. Essa é a definição de "epsi-
lon"(letra grega ) e é apenas dependente da precisão p, em bits, do tipo usado.
Citando o exemplo do float, p = 24, então  = 2−23 , ou seja, ele é igual a:

 = 1.1920928955078125 · 10−7
Isso faz sentido se observarmos que F , tendo p − 1 bits de tamanho tem
seu msb correspondente a 2−1 e, caminhando em direção ao bit de baixa ordem
temos o lsb correspondente a 21−p . Por exemplo, considerando e = 0 o valor
binário da mantissa 0b1.1000 · · · corresponde a 1 + 2−1 = 1.5, já 0b1.01 · · ·
a 1.25 – e se observarmos o primeiro exemplo de encontrarmos os valores e
e F , lá em cima, veremos que achamos um F = 2097152 ou, em binário,
0b01000000000000000000000 (23 bits).
O  só se aplica a faixa entre [1, 2), ou seja, quando e = 0. Precisamos
multiplicá-lo pelo fator de escala, 2e , para obtermos o menor incremento possível
para a escala em uso. Esse menor valor possível de acordo com a escala é
chamado de ulp ou "Unit at Last Position"4 . A ulp é, então, calculada com
base na precisão p e no expoente e:

ulp(e) =  · 2e = 2e−p+1 (7)


Note que se temos e = 0 obtemos . E, é claro, uma ulp pode ser calculada
em função de n, ao invés de e, se o substituirmos pela equação 5:

ulp(n) = 2blog2 |n|c−p+1


Aqui vai outro exemplo, consideremos n = 100000.0 em um tipo float e,
portanto, p = 24 e e = 16. Sendo n um valor inteiro que ocupa menos que
24 bits, cabe perfeitamente na mantissa sem necessitar ser arredondado. O
próximo valor mais perto dele que pode ser representado em um float é de
1 ulp adiante, ou seja, ulp(16) = 216−24+1 = 2−7 . Então, o próximo valor
representável nessa escala é 100000 + 2−7 ou 100000.0078125. Observe que um
valor como 100000.005 não pode ser representado nessa escala.
Quanto maior a escala, maior será o valor correspondente a uma ulp. Outro
exemplo poderia ser o mesmo float contendo um valor como 1015 . Nesse caso
e = 49, nos dando uma ulp de 67108864. Nessa escala esse é a menor diferença
entre valores adjacentes de n.
É evidente que quando maior a precisão, menor será uma ulp. Tomando o
exemplo anterior usando o tipo double, p = 53, temos uma ulp correspondendo
a 2−3 = 0.125, ao invés de 67108864. Num long double seria 6.103515625·10−5 .

Valores sub normais


Quando e = −Ebias , ou E = 0, é interpretado como e = 1 − Ebias e aquele
valor inteiro unitário somado à fração contendo F desaparece. Para esse valor
4A unidade da última posição (ou bit) de F , onde "última"é o lsb

5
especial de e, a seguinte equação descreve o valor final de n:

S F
n = (−1) · · 21−Ebias
2p−1
Aqui vou desconsiderar o bit de sinal, como no caso da equação 4:
F
n= · 21−Ebias (8)
2p−1
Esses valores são chamados de sub normais porque valores normalizados têm
aquele 1 somado ao termo da fração. No caso de valores sub normais a mantissa
fica, em binário, como sendo 0b0.F .
Sub normais nos dão valores entre zero (para F = 0) e o valor máximo
dependerá da precisão p. Para um floats o fator de escala será 2−126 , o que
nos dá, para Fmax = 223 − 1:

223 − 1
nmax = · 2−126 = 2−126 − 2−149 ≈
223
1.1754942106924... · 10−38
E uma ulp também é fixa para valores sub normais, de acordo com a precisão,
já que estamos lidando com uma faixa entre [21−Ebias , 22−Ebias ). No caso de
um float, uma ulp será 2−149 ou, aproximadamente, 1.40129846432... · 10−45 .
Esse é o menor valor possível, diferente de zero, que pode ser armazenado num
float. A especificação ISO 9899 cita um valor diferente, mas lá é levado em
conta o menor valor possível normalizado, que é obtido com E = 1 e F = 0,
ou seja, aproximadamente, 1.17549435... · 10−38 .
Até o momento não podíamos representar o valor zero. O único jeito é usando
uma representação "sub normal", que não é tratada como tal. A representação
de zero é feita com E = 0 e F = 0, da equação 8:
 
0
n= · 21−Ebias = 0.0
2p−1
O que leva a outro problema: O bit de sinal continua valendo e temos agora
+0 e -0. Outra coisa: Exceto pelo zero, todos valores sub normais exigem pro-
cessamento extra para que operações aritméticas sejam computadas, causando
um pequeno atraso.
Felizmente a faixa dos sub normais é apenas uma dentre todas as Emax − 1
faixas possíveis. A ideia dessa faixa, além de possibilitar a representação de
zero, é oferecer uma alternativa elegante aos underflow s. No caso de operações
resultarem em menos que 1 ulp de um sub normal o processador pode sinalizar
uma exceção de "inexatidão"5 , mas a reposta será zero.
5 Exceções em ponto flutunte não constituem, necessariamente, em "interrupções"do fluxo

do programa. Pense nelas mais como uma espécie de flags, mantidos pelo hardware.

6
Valores que não são números
Diferente dos sub normais, valores onde E = Emax são chamados de NaN s –
acrônimo de Not a Number. Existem duas classes de NaN s possíveis: Infinitos
e valores inválidos, estranhamente também chamados de NaN s.
Infinitos ocorrem como consequência de overflows e também em operações
como divisões por zero. Embora isso seja matematicamente estranho, se divi-
dirmos um valor numérico diferente de zero pelo próprio zero, obtemos infinitos
com o sinal de acordo com a divisão (considerando o sinal do zero também).
Mas, se dividirmos zero por zero, obteremos um NaN. Existe também a exceção
de "divisão por zero"para indicar essa tentativa.
Outro exemplo de obtenção de NaN como resultado é a tentativa de extrair
a raiz quadrada de valores negativos ou obter o logaritmo, em qualquer base,
do valor zero.
Uma propriedade dos NaN s, mas não dos infinitos, é que qualquer compara-
ção com eles resulta sempre em falso, não importa o sinal, exceto a comparação
por diferença, Nenhum valor válido é obviamente igual a um NaN e nem mesmo
um NaN é igual a outro NaN. Afinal, um NaN não é um número e não deveria
ser comparável a um ou a algo que não é um número:

1 /* test . c */
2 # include < stdio .h >
3 # include < math .h >
4

5 # define test ( c ) (( c ) ? " SIM " : " NAO " )


6

7 int main ( void )


8 {
9 printf ( " NAN == NAN ? % s \ n " ,
10 test ( NAN == NAN ) );
11

12 printf ( " - NAN < + NAN ? % s \ n " ,


13 test ( - NAN < NAN ) );
14

15 printf ( " INFINITY == INFINITY ? % s \ n " ,


16 test ( INFINITY == INFINITY ) );
17

18 printf ( " - INFINITY < + INFINITY ? % s \ n " ,


19 test ( - INFINITY < INFINITY ) );
20 }
Listagem 1: Teste com NAN s.

Isso resulta em:

NAN == NAN? NAO


-NAN < +NAN? NAO
INFINITY == INFINITY? SIM
-INFINITY < +INFINITY? SIM

7
Embora você possa testar se um objeto contenha um NaN ou não usando
o operador ==, essa não é a melhor maneira porque o campo F em um NaN,
mesmo sem significado, pode ser qualquer um. A maneira correta de determinar
se um valor é ou não um NaN é usando a função/macro isnan(), definida em
math.h. Isso não acontece com infinitos porque, no caso deles, F = 0, mas
também existe uma função/macro isinf().

Cuidado com overflows


Como vimos, overflows podem resultar em NaN s, mesmo que o fator de
escala do tipo permita a representação de valores extremamente grandes. Em
alguns casos é prudente modificar um pouquinho o que se pretende para evitar
esse "estouro".
Um exemplo clássico é o teorema de Pitágoras:
p
h2 = a2 + b2 ⇒ h = a2 + b2
Dependendo do valor de a e b podemos ter um overflow nessa elevação ao
quadrado e na adição. Uma maneira de evitar isso é modificar a fórmula, to-
mando certos cuidados. Se considerarmos a > b e a 6= 0, poderíamos reescrever
a fórmula como:
r
a2
h = (a2 + b2 ) 2
a
r
a2 + b2
h = |a|
a2
s  2
b
h = |a| 1 +
a
2
No caso de a = 0 teremos h = |b|. Note que ab , já que a > b, sempre
resultará num valor menor ou igual a 1, impossibilitando o overflow. A fórmula
final fica, então:
( q
b 2

h = |a| 1 + a , a 6= 0
|b| , a=0

Sobre o estranho tipo long double


Na figura 1 vimos que o tipo long double, ao contrário dos demais, tem
o bit inteiro da mantissa, implícito nas outras representações, representado de
forma explícita na sua estrutura. Isso pode causar alguns problemas:

• E se esse bit estiver zerado e 0 < E < Emax ? Ele é ou não "normalizado"?

8
• E se esse bit estiver setado e E = 0? Teríamos um valor sub normal ?
De forma geral, o tipo long double deve ser evitado. Lidar com essas duas
perguntas acima pode levar a erros "esquisitos"de cálculo somente porque os
tipos normalizados e sub normais podem ser "corrompidos"e, além do mais,
essa representação não é, geralmente, suportada em SIMD6 .

A extensão do GCC/clang: __float128


Esse tipo é previsto pelo IEEE 754 como sendo "duplamente estendida"e é
implementado no GCC/clang, mas sem recursos de hardware. Todas as ope-
rações lidando com esse tipo são feitas por software através de uma série de
funções built in dos compiladores.
O tipo __float128 tem, no fator de escala, o mesmo tamanho do expoente
do tipo long double (15 bits), mas tem precisão p de 113 bits. Diferente do
long double, o bit implícito da mantissa é "implícito", como nos tipos float
e double.
Com um  = 2−112 , isso torna o tipo desejável quando queremos peque-
níssimos ulps para faixas cada vez maiores. Só tome cuidado: As operações
são extremamente lentas e não há suporte nativo para o tipo em funções da
biblioteca padrão. Embora você possa efetuar as operações fundamentais que
o compilador provê, para usar __float128 você pode ter que usar também
outra biblioteca distribuída junto com o GCC (e clang também?) chamada
libquadmath.
Só para constar, valores literais do tipo __float128 têm o sufixo Q, como
em:

1 __float128 x = 3.15 Q ;
Listagem 2: Exemplo de inicialização de __float128.

A "precisão"mínima em decimal para ponto flutu-


ante
Até aqui o conceito de "precisão"aplica-se apenas à quantidade de bits da
mantissa, mas e quanto aos valores decimais? Ora, se temos p bits de precisão
isso significa que o máximo valor de F só pode ser Fmax = 2p−1 −1. Adicionando
o bit integral implícito poderíamos reescrever a equação 4 como:
 p−1 
2 +F
n= · 2e
2p−1
É fácil perceber que na aplicação da equação 4 um valor n qualquer nem sem-
pre pode ser exatamente expresso. Então, qual seria a quantidade de algarismos
6 Pode ser que exista algum processador que a suporte. Desconheço.

9
decimais garantidamente "precisos"? Se consideramos que estamos trabalhando
com binários, e temos precisão de p bits, então podemos inferir que:

2p = 10k
Para encontrarmos o expoente k que seja uma representação exata do valor
em base 2. Acontece que p é um valor inteiro (expresso em bits) e só podemos
obter um k inteiro, expresso em algarismos. Isso nos dá:

k = blog10 2p c
k = bp · log10 2c
Assim, um float tem precisão mínima decimal de 7 algarismos, double tem
15 e long double, 19.
Note que estou usando o termo "precisão"no mesmo sentido anterior. No
caso, a "precisão binária"nos dá a quantidade de bits de F mais 1. No caso
da "precisão decimal"isso nos dá a menor quantidade de algarismos que cor-
responde a um valor "exato". Por exemplo, considere um float cujo valor é
0.1. Esse valor, como já vimos, é impossível de ser representado exatamente
em ponto flutuante. A mantissa, em binário, é 0b1.10011001100 · · · , ad inifini-
tum, e portanto ela tem que ser arredondada. Se usarmos apenas 7 algarismos
decimais o valor que obtivemos no exemplo, lá em cima, será arredondado cor-
retamente para 0.1000000. À partir do 8º algarismo começamos a obter valores
"não exatos", por causa da ulp.
É bom notar que a função printf() da biblioteca padrão de C não faz
arredondamentos, ela trunca o valor impresso com base na "precisão decimal
depois da ’vírgula’"e que "precisão"para o printf() aplica-se à parte fracionária
do valor. Por default printf() usa uma "precisão"de 6 algarismos7 na parte
fracionária.

7 Você pode usar quantos algarismos "depois da vírgula"quiser com printf. Trata-se de

uma rotina que imprime um ponto flutuante em uma string decimal. Não estamos, de fato,
lidando com valores decimais.

10

Você também pode gostar