2025-06-18 09:33:28 -03:00
#+title : 9 -- Conversão de representações de ponto flutuante
2025-06-07 12:52:07 -03:00
#+author : Blau Araujo
#+email : cursos@blauaraujo.com
#+options : toc:3
* Objetivos
2025-06-18 09:33:28 -03:00
- Conhecer as formas de codificação binária de números de ponto flutuante.
- Identificar as representações de números de ponto flutuante com strings.
- Implementar funções e sub-rotinas para converter os representações de
ponto flutuante nos valores representados.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
* Tipos de ponto flutuante em C
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A linguagem C define três tipos principais de ponto flutuante (x86_64):
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
| Tipo | Precisão | Tamanho |
|-------------+-----------+----------|
| =float= | Simples | 4 bytes |
| =double= | Dupla | 8 bytes |
| =long double= | Estendida | 16 bytes |
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A precisão refere-se à quantidade de dígitos que podem ser representados de
forma exata em um número, sendo um parâmetro essencial para determinar a
precisão de resultados de operações aritméticas, ate onde podemos representar
números muito pequenos sem que eles virem zero e distinguir valores em
operações de comparação.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
No entanto, observe o que acontece com a execução do exemplo abaixo...
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Arquivo: [[exemplos/10/precision.c ][precision.c ]]
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
#+begin_src c :tangle exemplos/10/precision.c
2025-06-07 12:52:07 -03:00
#include <stdio.h >
2025-06-18 09:33:28 -03:00
#include <float.h > // Para as precisões
2025-06-07 12:52:07 -03:00
int main(void) {
2025-06-18 09:33:28 -03:00
float f = 10.0f / 3.0f;
double d = 10.0 / 3.0;
long double ld = 10.0L / 3.0L;
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
printf("float (%d): %.20f\n", FLT_DIG, f);
printf("double (%d): %.20lf\n", DBL_DIG, d);
printf("long double (%d): %.20Lf\n", LDBL_DIG, ld);
2025-06-07 12:52:07 -03:00
return 0;
}
#+end_src
2025-06-18 09:33:28 -03:00
Aqui, nós queremos imprimir os resultados das divisões utilizando os três
tipos de ponto flutuante e, segundo as definições da =glibc= , quantos dos seus
dígitos decimais teriam precisão garantida. Para isso, estamos utilizando as
constantes simbólicas do cabeçalho =float.h= :
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- =FLT_DIG= : Quantidade de dígitos decimais garantidos no tipo =float= .
- =DBL_DIG= : Quantidade de dígitos decimais garantidos no tipo =double= .
- =LDBL_DIG= : Quantidade de dígitos decimais garantidos no tipo =long double= .
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Compilando e executando:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
:~$ gcc -Wall precision.c
:~$ ./a.out
float (6): 3.33333325386047363281
double (15): 3.33333333333333348136
long double (18): 3.33333333333333333326
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
De fato, os resultados são precisos até os dígitos decimais esperados:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
6
|
float (6): 3.33333325386047363281
15
|
double (15): 3.33333333333333348136
18
|
long double (18): 3.33333333333333333326
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
* Representações em expressões constantes
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Ainda no código do exemplo, observe como os valores de ponto flutuante foram
escritos:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
#+begin_src c
float f = 10.0f / 3.0f;
double d = 10.0 / 3.0;
long double ld = 10.0L / 3.0L;
#+end_src
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Tivemos que escrever assim porque, por padrão, expressões constantes com ponto
decimal são avaliadas como =double= , a menos que sejam seguidas de =f= (=float= ) ou
=L= (=long double= ), como podemos ver nesta tabela, que inclui as notações com
expoente:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
| Literal | Tipo |
|------------------+-------------|
| =3.14= | =double= |
| =3.14f= ou =3.14F= | =float= |
| =3.14l= ou =3.14L= | =long double= |
| =3e[+/-]2= | =double= |
| =3e[+/-]2f= (ou =F=) | =float= |
| =3e[+/-]2l= (ou =L=) | =long double= |
2025-06-07 12:52:07 -03:00
#+begin_quote
2025-06-18 09:33:28 -03:00
Também existem as representações com expoentes binários, introduzidos no C99
para representar valores em hexadecimal com expoente em base 2 (ex.: =0x1.8p1= ),
mas eles não serão estudados neste curso.
2025-06-07 12:52:07 -03:00
#+end_quote
2025-06-18 09:33:28 -03:00
Para os nossos propósitos, porém, a representação literal da linguagem C que
nos interessa é a do tipo =double= (=3.14= ou =0.42= ) com ou sem sinal. Sobre isso,
valores negativos são representados da mesma forma que os positivos, mas com
o bit de sinal ativado.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Sendo assim, os dígitos dos valores convertidos pelas nossas implementações
deverão corresponder a todos os caracteres da string numérica e serão
retornados como =double= , o que implica:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- Valor retornado em 8 bytes (64 bits) com ou sem sinal;
- Limite máximo em DBL_MAX
- Limite mínimo em DBL_TRUE_MIN (menor subnormal).
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
* Codificação binária do tipo double (64 bits)
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Um número de ponto flutuante com dupla precisão (/double/ ) ocupa 64 bits
divididos em três campos:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
63 62 51 0
┌───┬──────────┬────────────────────────────┐
│ S │ EXPOENTE │ MANTISSA │
└───┴──────────┴────────────────────────────┘
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
De modo a representar números da faixa normal (parte inteira ~>=1~ ) como:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
(-1)^sinal × (1 + (mantissa/2^52)) × 2^(expoente - 1023)
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
#+begin_quote
*Nota:* /mantissa/ é como chamamos a parte fracionária do número.
#+end_quote
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Bits de expoente
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
O expoente diz quantas vezes o valor da mantissa será multiplicado ou
dividido por 2. O campo possui 11 bits (bits 62 a 52) e pode receber valores
entre 0 e 2047 que assumirão três significados diferentes:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
| Expoente | Significado |
|----------+-----------------------------------------|
| =0= | Número muito pequenos (subnormais) ou =0= |
| =2047= | Infinito ou não é um número (=NaN=) |
| =1= a =2046= | Números na faixa normal |
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Se o expoente for =0= , ele será interpretado com o valor fixo =-1022= e a
interpretação será feita sem o bit implícito, resultando na expressão
de valores iniciados com =0.= alguma coisa. Neste caso, nós teremos valores
muito pequenos, abaixo dos valores normais e, por isso mesmo, chamados de
/subnormais/ .
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*Exemplos:*
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
0x0000000000000000 => +0.0
0x0000000000000001 => Menor subnormal positivo
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
Já a interpretação do expoente =2047= depende do que está na mantissa: se for
=0= , o número é interpretado como infinito; caso contrário, não será um
número.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*Exemplos:*
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
0x7ff0000000000000 => +Infinito (0x7ff = 2047)
0x7ff0000000000001 => NaN
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
Quando o expoente representa números na faixa normal, a interpretação dos
64 bits deve ser feita considerando como se existisse um bit 1 antes da
mantissa, garantindo que a parte inteira do número normalizado nunca seja
zero.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*Exemplo:*
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
#+begin_example
123.456 => 0x405edd2f1a9fbe77
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Primeiros 12 bytes:
0x405 = 0100 0000 0101
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
bit de sinal (+1)
↓
0 10000000101
↑
11 bits do expoente (0x405 => 1029)
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Expoente real:
1029 - 1023 = 6 = > 2^6 = 64
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Mantissa:
0xedd2f1a9fbe77
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Mantissa real:
1 + 0xedd2f1a9fbe77 / 2^52 = 1.929
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Valor final:
+1 × 1.929 × 64 = +123.456
#+end_example
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
* Codificação binária do tipo float (32 bits)
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
O tipo /float/ armazena números reais com precisão simples em 32 bits, também
divididos em três campos:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
31 30 22 0
┌───┬──────────┬────────────────────────────┐
│ S │ EXPOENTE │ MANTISSA │
└───┴──────────┴────────────────────────────┘
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
Deste modo, valores normalizados (parte inteira ~>=1~ ) podem ser calculados
pela fórmula:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
(-1)^sinal × (1 + (mantissa/2^23)) × 2^(expoente - 127)
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
** Bits de expoente
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
O expoente é expresso com 8 bits, o que pode resultar em valores de 0 a 255
com os seguintes significados:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
| Expoente | Significado |
|----------+-----------------------------------|
| =0= | Número subnormal (=0.xxx=) ou zero. |
| =1= a =254= | Números na faixa normal (=1.xxx=). |
| =255= | Infinito ou NaN. |
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Assim como acontece na representação binária do tipo /double/ , expoentes entre
=1= e =254= implicam a soma de 1 à mantissa real (=mantissa/2^23= ) na interpretação
do valor representado. Com expoente =0= , a mantissa real ainda é utilizada para
obter a parte fracionária de um número subnormal (=0.xxx= ). Contudo, se o
expoente for =255= , utiliza-se a mantissa armazenada para decidir se o valor é
infinito (mantissa =0= ) ou não é um número (mantissa diferente de =0= ).
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
* Unidades de ponto flutuante em x86_64
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A arquitetura x86_64 representa e manipula números reais por meio de três
mecanismos históricos:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- FPU (/Float-Point/ Unit): coprocessador matemático;
- Instruções SSE/SSE2: suporte a operações com ponto flutuante;
- Registradores XMM: utilizados com instruções SSE.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A FPU (x87) surgiu em 1980 com o coprocessador 8087, mais tarde integrado ao
processador 486DX. Ainda é suportado na arquitetura x86_64 por compatibilidade,
mas raramente é utilizado em códigos modernos -- a não ser para lidar com
=long double= (números de ponto flutuante com 128 bits).
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
As instruções SSE/SSE2 foram introduzidas nos processadores Pentium III e IV,
dos anos 2000, como mecanismos preferenciais para operações de ponto flutuante.
A partir da arquitetura x86_64, as instruções SSE2 tronam-se obrigatórias e
substituem praticamente toda a FPU em compiladores modernos.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Mais recentemente, os processadores passaram a oferecer extensões como AVX
(Advanced Vector Extensions), que ampliam o número e a largura de registradores
XMM (passando a YMM e ZMM, com 256 e 512 bits) e otimizam operações vetoriais
e de ponto flutuante em larga escala. Essas instruções são usadas por
compiladores modernos em programas de alto desempenho.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
#+begin_quote
*Nota:* os registradores de propósito geral nunca foram projetados para trabalhar
com ponto flutuante. Mas, ao utilizá-los nas nossas demonstrações de conversões,
nossos objetivos são apresentar o núcleo lógico dessas operações e demonstrar
os problemas e as limitações que os recursos modernos superam.
#+end_quote
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
* Conversão de strings para float
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Ao converter uma string como ="12.75"= para um número real no Assembly, as
principais abordagens para representar o resultado são:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- Formato /escalado/ (também chamado de /fixed-point/ );
- Formato empacotado como número IEEE 754 de 32 bits (/float/ ).
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Ambas as abordagens são úteis em contextos distintos e possuem vantagens e
limitações que devem ser compreendidas e consideradas.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Alternativa 1: Representação escalada (fixed-point)
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Em Assembly (especialmente sem FPU), não há suporte direto para números com
parte fracionária. Mas é possível simular valores reais utilizando inteiros,
contanto que todos os números sejam representados na mesma /escala/ . A ideia é
preservar as casas decimais multiplicando o valor original por um fator
fixo (como =2^23= ) e armazenar o resultado como um inteiro.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Por exemplo, podemos representar o número =12.34= como:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
12.75 × 2^23 = 1065353216
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
#+begin_quote
O exemplo de fator =2^23= (ou =8.388.608= ) está relacionado com o fato deste ser
o número de bits disponíveis para representar a mantissa em um número /float/
de 32 bits no padrão IEEE 754, como vimos.
#+end_quote
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
O valor em escala pode ser armazenado em um registrador de propósito geral e
operado com instruções inteiras comuns, como =add= , =sub= , =imul= , =idiv= , etc. Após
a operação, o resultado pode ser interpretado diretamente em escala ou
/desescalado/ para a forma de ponto flutuante.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Por exemplo, numa conversão de Celsius para Fahrenheit...
2025-06-07 12:52:07 -03:00
#+begin_src asm
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
; RAX = Temperatura em Celsius × 2^23
; Conversão para Fahrenheit: F = C × 9/5 + 32
2025-06-07 12:52:07 -03:00
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
imul rax, rax, 9 ; rax = rax × 9
cqo ; estende o sinal de rax em rdx (para imul)
mov rcx, 5 ; divisor
idiv rcx ; rax = rax / 5
add rax, 32 << 23 ; rax = rax + (32 × 2^23)
; Resultado final: Fahrenheit × 2^23
2025-06-07 12:52:07 -03:00
#+end_src
2025-06-18 09:33:28 -03:00
Para exibir o resultado como uma string, bastaria dividi-lo por =2^23=
(=8.388.608= ). Em Assembly, podemos utilizar =mov= e =idiv= para obter parte
inteira e parte decimal separadas antes de fazer a conversão para texto...
2025-06-07 12:52:07 -03:00
#+begin_src asm
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
; RAX = Fahrenheit × 2^23
2025-06-07 12:52:07 -03:00
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
mov rbx, rax ; copia valor escalado
mov rcx, 8388608 ; fator da escala (2^23)
xor rdx, rdx ; zera rdx para o resto
idiv rcx ; rax = parte inteira, rdx = parte fracionária (resto)
; converter a partte inteira para string...
2025-06-07 12:52:07 -03:00
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
; A parte fracionária ainda está em escala...
2025-06-07 12:52:07 -03:00
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
mov rbx, 1000 ; multiplicador da parte fracionária
imul rdx, rbx ; x1000 => um inteiro desescalado com 3 algarismos
idiv rcx ; rax = parte fracionária
; converter a parte fracionária para string e concatenar com '.'...
2025-06-07 12:52:07 -03:00
#+end_src
2025-06-18 09:33:28 -03:00
A grande vantagem dessa abordagem é sua simplicidade e compatibilidade com
qualquer conjunto de instruções, sem depender da FPU ou de registradores XMM.
Contudo, ela exige que todas as operações preservem a escala, o que requer
muita atenção com as operações aritméticas. Ao final, o resultado pode ser
convertido para exibição ou empacotado como /float/ para ser usado com
instruções de ponto flutuante ou passado para funções externas da Glibc.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Alternativa 2: Representação IEEE 754
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A representação empacotada como float segue o padrão IEEE 754. Um número como
=12.75= , por exemplo, seria convertido para a forma binária através dos
seguintes passos:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- Determinar o sinal (0 para positivo);
- Converter o número para binário normalizado;
- Calcular o expoente com o bias (deslocamento);
- Codificar a mantissa (sem o bit implícito).
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*Passo 1: determinar o sinal*
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
O número é positivo, então o sinal é =0= .
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*Passo 2: converter o número para binário normalizado*
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- Parte inteira (12): =11000= em base 2.
- Parte fracionária (0.75): =11= , obtido por multiplicações sucessivas por 2.
- Resultado da conversão: =1100.11=
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Normalizar um binário de ponto flutuante é representá-lo como:
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
1.xxxxxx × 2^n
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
No caso de =1100.11= , será preciso deslocar o ponto 3 posições para a esquerda.
Portanto, para manter seu valor original, nós temos que multiplicá-lo por
=2^3= :
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
1100.11 = 1.10011 × 2^3
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
*Passo 3: calcular o expoente com bias*
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Como o deslocamento da normalização foi de =2^3= , o expoente com /bias/ será:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
#+begin_example
3 + 127 (bias IEEE 754) = 130
130 = 10000010 na base 2
#+end_example
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*Passo 4: codificar a mantissa*
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A mantissa é a parte fracionária do valor normalizado em binário seguida de
tantos zeros quanto forem necessários para totalizar 23 bits, portanto...
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
Mantissa de 1.10011 => 10011
Mantissa armazenada => 10011000000000000000000
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
*Empacotamento dos campos*
2025-06-07 12:52:07 -03:00
#+begin_example
2025-06-18 09:33:28 -03:00
Sinal : 0 (positivo)
Expoente : 10000010 (130 = 127 + 3)
Mantissa : 10011000000000000000000
Resultado: 01000001010011000000000000000000 => 0x41480000 em hexa
2025-06-07 12:52:07 -03:00
#+end_example
2025-06-18 09:33:28 -03:00
A forma empacotada é utilizada em operações de ponto flutuante com SSE/FPU
e para passar argumentos =float= para funções em C. Ela permite o uso de
instruções especializadas da arquitetura, mas requer decodificação
e empacotamento.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Quando optar por uma ou outra alternativa
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Via de regra, nós só empacotamos para o formato IEEE 754 quando realmente
precisamos interagir com outros componentes que exijam o tipo /float/ . Nas
situações mais comuns, a representação em escala é mais que suficiente.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
| Objetivo | Formato recomendado |
|---------------------------------+---------------------|
| Aritmética simples com inteiros | Escalado (/fixed/) |
| Comparações e fórmulas básicas | Escalado (/fixed/) |
| Compatibilidade com SSE/FPU | IEEE 754 |
| Chamada de função com "%f" | IEEE 754 |
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Dificuldades comuns
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*** 1. Arredondamento de frações
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Ao representar "0.333" como inteiro escalado, há perda de precisão. Aumentar o
fator de escala (por exemplo, 2^30 em vez de 2^23) melhora a precisão, mas pode
causar /overflow/ em multiplicações.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*** 2. Erros ao interpretar IEEE como inteiro
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Se usarmos um /float/ empacotado (como =0x41200000= , que representa =10.0= ) em uma
operação inteira, o valor será incorreto. Isso ocorre porque o padrão IEEE é
uma codificação binária, e não o valor numérico diretamente.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
*** 3. Falta de suporte a ponto flutuante
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Alguns ambientes minimalistas, como bootloaders ou sistemas bare-metal,
podem não ter suporte a SSE/FPU. Nestes casos, a representação escalada é
a única alternativa viável.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Diferenças na conversão para double (64 bits)
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
O tipo /double/ (também do padrão IEEE 754) ocupa 64 bits e permite maior
precisão e maior faixa de representação. Sua estrutura é similar à de um
/float/ , mas com:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- 1 bit para o sinal
- 11 bits para o expoente (~bias=1023~ )
- 52 bits para a mantissa (com bit 1 implícito)
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Sendo assim, as operações de empacotamento e desempacotamento seguem o mesmo
processo do /float/ , mas precisam trabalhar com registradores de 64 bits
completos.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
A representação escalada também pode ser estendida para o caso de valores que
eventualmente serão convertidos para /double/ , com os mesmos benefícios de
usar apenas instruções inteiras durante o processamento. Contudo, quando
demonstramos o uso do tipo /float/ , nós adotamos o fator de escala =2^23= ,
por ser compatível com o número de bits disponíveis para a mantissa. No caso
de /double/ , porém, a mantissa tem 52 bits, o que permitiria escalar até =2^52=
(aproximadamente 4.5 × 10^15) sem perda de precisão por arredondamento.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
No entanto, escalar por =2^52= pode causar overflow em algumas multiplicações
se o valor base for muito grande. Assim, uma alternativa prática seria
escalar por =2^30= , =2^32= ou outro fator suficientemente alto, mas ainda seguro
para uso com registradores de 64 bits.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
** Sub-rotinas para demonstração
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Aqui estão duas sub-rotinas:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- =str_to_float_scaled= : converte strings no formato =<int>.<frac>= para
representações de números de ponto flutuante como inteiros de 32 bits
escalados.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- =str_to_float_ieee754= : converte strings no formato =<int>.<frac>= para
representações de números de ponto empacotados no formato IEEE 754 de
32 bits.
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
Embora seja possível que elas funcionem corretamente, nenhuma delas chegou
a ser testada. Portanto, o exercício proposto para esta aula será:
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- Analisar o código de cada uma delas;
- Compreender e explicar as decisões tomadas;
- Estudar as instruções ainda desconhecidas;
- Elaborar formas de testá-las;
- Corrigir eventuais erros;
- Buscar formas de otimizá-las;
- Implementar versões para 64 bits (/double/ ).
*** =str_to_float_scaled=
2025-06-07 12:52:07 -03:00
#+begin_src asm
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
; Converte uma string no formato <int >.<uint > para float
; Entrada: RDI -> ponteiro para string válida
; Saída: EAX = número codificado como float (IEEE 754, 32 bits)
; Altera: RBX, RCX, RDX, RSI, R8, R9, R10
2025-06-07 12:52:07 -03:00
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
str_to_float_scaled:
2025-06-07 12:52:07 -03:00
; ----------------------------------------------------------
2025-06-18 09:33:28 -03:00
push rbx ; salva rbx na pilha
push rsi ; salva rsi na pilha
mov rsi, rdi ; salva ponteiro da string em rsi
xor rbx, rbx ; RBX = parte inteira
xor rcx, rcx ; RCX = parte fracionária
xor rdx, rdx ; RDX = divisor decimal (potência de 10)
xor r8d, r8d ; sinal (0 = positivo, 1 = negativo)
; --------------------------------------------------
; Verifica e armazena o sinal
; --------------------------------------------------
mov al, byte [rsi]
cmp al, '-'
jne .check_plus
inc rsi
mov r8b, 1
jmp .read_integer
2025-06-07 12:52:07 -03:00
.check_plus:
2025-06-18 09:33:28 -03:00
cmp al, '+'
jne .read_integer
inc rsi
; --------------------------------------------------
; Lê parte inteira: <int >
; --------------------------------------------------
.read_integer:
xor eax, eax ; eax = 0
.read_int_loop:
mov al, byte [rsi]
cmp al, 0 ; fim da string
je .build_float
cmp al, '.' ; fim da parte inteira
je .read_fraction
sub al, '0' ; converte dígito em número
jb .build_float ; se al < 0x30, fim do número
cmp al, 9
ja .build_float ; se al > 9, fim do número
imul rbx, rbx, 10 ; rbx = rbx * 10
add bl, al ; bl = bl + al
inc rsi
jmp .read_int_loop
; --------------------------------------------------
; Lê parte decimal: .<uint >
; --------------------------------------------------
.read_fraction:
inc rsi ; pula o ponto
xor ecx, ecx ; zera a parte decimal
mov edx, 1 ; divisor = 1
.read_frac_loop:
mov al, byte [rsi]
cmp al, 0 ; número é xxx.0
je .build_float
sub al, '0' ; converte dígito em número
jb .build_float ; se al < 0x30, fim do número
cmp al, 9
ja .build_float ; se al > 9, fim do número
imul rcx, rcx, 10 ; rcx = rcx * 10
add cl, al ; cl = cl + al
imul rdx, rdx, 10 ; divisor *= 10
inc rsi
jmp .read_frac_loop
; --------------------------------------------------
; Combina: float = inteiro + (fracao / divisor)
; rbx = parte inteira (int)
; rcx = parte fracionária (uint)
; rdx = divisor decimal
; converte parte inteira para float (em eax)
; --------------------------------------------------
.build_float:
mov eax, ebx
test r8b, r8b
jz .skip_negate_int ; número é positivo
neg eax ; aplica sinal de negativo
; --------------------------------------------------
; converte parte decimal: (rcx / rdx) → aproximação inteira × escala
; Aqui usamos multiplicação por 2^n e bit shift para simular uma fração
; Exemplo: 123 / 1000 ≈ (123 << 23) / 1000
; --------------------------------------------------
.skip_negate_int:
shl rcx, 23
xor r9d, r9d
div rdx ; rcx / rdx (com resto em rdx), quociente em rax
mov r9d, eax ; parte decimal em r9d
; --------------------------------------------------
; soma a parte inteira com a parte decimal (ambos em "escala flutuante")
; --------------------------------------------------
shl eax, 23 ; inteiro convertido para a mesma escala
add eax, r9d ; eax = pf aproximado * 2^23
pop rsi
pop rbx
ret
2025-06-07 12:52:07 -03:00
#+end_src
2025-06-18 09:33:28 -03:00
*** =str_to_float_ieee754=
#+begin_src asm
; ----------------------------------------------------------
; Converte uma string no formato <int >.<uint > para float IEEE 754 (32 bits)
; Entrada: RDI -> ponteiro para string válida
; Saída: EAX = número codificado como float (IEEE 754, 32 bits)
; Altera: RBX, RCX, RDX, RSI, R8, R9, R10
; ----------------------------------------------------------
str_to_float_ieee754:
; ----------------------------------------------------------
push rbx
push rsi
mov rsi, rdi ; ponteiro da string
xor rbx, rbx ; parte inteira
xor rcx, rcx ; parte fracionária
xor rdx, rdx ; divisor decimal (potência de 10)
xor r8d, r8d ; sinal (0 = positivo, 1 = negativo)
; --------------------------------------------------
; Sinal
; --------------------------------------------------
mov al, byte [rsi]
cmp al, '-'
jne .check_plus
inc rsi
mov r8b, 1
jmp .read_int
.check_plus:
cmp al, '+'
jne .read_int
inc rsi
; --------------------------------------------------
; Parte inteira
; --------------------------------------------------
.read_int:
xor eax, eax
.read_int_loop:
mov al, byte [rsi]
cmp al, 0
je .build_scaled
cmp al, '.'
je .read_frac
sub al, '0'
jb .build_scaled
cmp al, 9
ja .build_scaled
imul rbx, rbx, 10
add bl, al
inc rsi
jmp .read_int_loop
; --------------------------------------------------
; Parte fracionária
; --------------------------------------------------
.read_frac:
inc rsi
xor ecx, ecx
mov edx, 1
.read_frac_loop:
mov al, byte [rsi]
cmp al, 0
je .build_scaled
sub al, '0'
jb .build_scaled
cmp al, 9
ja .build_scaled
imul rcx, rcx, 10
add cl, al
imul rdx, rdx, 10
inc rsi
jmp .read_frac_loop
; --------------------------------------------------
; Combina: inteiro + (frac/divisor)
; Resultado com fator 2^23
; --------------------------------------------------
.build_scaled:
mov eax, ebx
test r8b, r8b
jz .skip_neg
neg eax
.skip_neg:
shl rcx, 23
xor r9d, r9d
div rdx ; rcx / rdx -> rax
mov r9d, eax ; parte fracionária escalada
shl eax, 23 ; inteiro * 2^23
add eax, r9d ; valor total em escala 2^23
; --------------------------------------------------
; IEEE 754: empacotamento
; --------------------------------------------------
xor ecx, ecx
test eax, eax
jns .abs_ready
neg eax
mov ecx, 1 ; sinal = 1
.abs_ready:
bsr r8d, eax ; MSB
mov edx, r8d
sub edx, 23
add edx, 127 ; bias
mov ebx, eax
mov ecx, r8d
sub ecx, 23
shl ebx, cl
and ebx, 0x7FFFFF ; mantissa
shl r8d, 31 ; sinal
shl edx, 23 ; expoente
or eax, edx
or eax, ebx
or eax, r8d
pop rsi
pop rbx
ret
#+end_src
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
* Referências
2025-06-07 12:52:07 -03:00
2025-06-18 09:33:28 -03:00
- [[https://www-users.cse.umn.edu/~vinals/tspot_files/phys4041/2020/IEEE%20Standard%20754-2019.pdf?utm_source=chatgpt.com ][IEEE Standard for Floating-Point Arithmetic ]]
- [[https://en.wikipedia.org/wiki/IEEE_754?utm_source=chatgpt.com ][Wikipedia: IEEE 754 ]]