Nos artigos anteriores, desbravamos o mundo dos números inteiros. Aprendemos que o computador usa zeros e uns para contar, que ele usa o Complemento de Dois para representar negativos e que ele possui limites claros de memória (Overflow).
Também tivemos um breve contato com as frações binárias, descobrindo, por exemplo, como mostrar que \(10.101_2\) equivale a \(5.625\) em decimal. Mas, até agora, fizemos essas conversões no papel, assumindo que a vírgula estava “fixa” em algum lugar.
Mas como o hardware, com seus pentes de memória limitados a 32 ou 64 bits, armazena números reais? Como ele guarda, na mesma estrutura de memória, a distância entre duas galáxias (\(2.5 \times 10^{19}\) km) e o diâmetro de um átomo (\(1 \times 10^{-10}\) m)?
A resposta não está em fixar a vírgula, mas em deixá-la flutuar. Bem-vindo ao padrão IEEE 754, a regra universal que dita como computadores lidam com números reais (Ponto Flutuante) e a razão pela qual, às vezes, \(0.1 + 0.2\) não é exatamente \(0.3\).
O Problema da Escala: Por que não usar Ponto Fixo? #
Imagine que você tem 32 bits para guardar um número. A abordagem mais ingênua seria dividir esse espaço ao meio: usaríamos 16 bits para a parte inteira e 16 bits para a parte fracionária. Isso é chamado de Ponto Fixo (Fixed Point).
Para muitas aplicações financeiras, isso funciona bem (afinal, centavos só precisam de duas casas decimais). Mas para a ciência e engenharia, isso é um desastre.
- O Problema do Gigante: Com 16 bits inteiros, o maior número que conseguimos representar é \(65.535\). Isso não serve nem para contar a população de uma cidade pequena, quanto mais distâncias astronômicas.
- O Problema do Microscópico: Com 16 bits de fração, a menor diferença que conseguimos medir é \(2^{-16}\) (aprox. \(0.000015\)). Isso é inútil para medir grandezas da física quântica.
Se tentarmos aumentar a parte inteira, perdemos precisão na fração. Se aumentarmos a fração, perdemos alcance nos inteiros. O cobertor é curto.
A Solução: Notação Científica Binária #
Para resolver isso, os engenheiros da computação adotaram a mesma estratégia dos cientistas: a Notação Científica.
Em decimal, não escrevemos \(0.000000123\). Escrevemos \(1.23 \times 10^{-7}\). Note que a posição do ponto decimal “flutua” dependendo do expoente.
O computador faz a mesma coisa, mas em binário. Ele armazena números no formato:
\[ \pm 1.m \times 2^e \]Onde:
- \(\pm\): É o sinal (positivo ou negativo).
- \(m\): É a mantissa (o “corpo” do número, ex: \(1.01101...\)).
- \(e\): É o expoente (que move a vírgula para a esquerda ou direita).
Ao separar o valor da escala, conseguimos representar números colossalmente grandes e infinitesimalmente pequenos usando a mesma quantidade de bits.
Nos primórdios da computação (como nos mainframes IBM System/370), era comum usar Base 16 (Hexadecimal) para ponto flutuante. A ideia era aumentar o alcance do expoente.
No entanto, o padrão IEEE 754 padronizou o uso da Base 2. Segundo o clássico artigo de David Goldberg, “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, a escolha da Base 2 é matematicamente superior devido ao conceito de Wobble (Oscilação).
Em Base 16, o primeiro dígito significativo pode ser 0001 (1) ou 1111
(15). Isso significa que podemos desperdiçar até 3 bits de precisão apenas para
representar o dígito líder (ex: 0001 tem 3 zeros à esquerda inúteis).
Já na Base 2, o primeiro dígito é sempre 1. Isso maximiza a precisão efetiva e garante que o erro relativo varie muito menos (oscilação menor) do que em bases maiores.
A Anatomia do Ponto Flutuante (E por que escolhemos 32 bits) #
Antes de dissecarmos os bits, precisamos de um pouco de contexto. Até o início da década de 80, cada fabricante de computador (IBM, Cray, DEC) tinha seu próprio formato para números reais. Um código que rodava em um mainframe poderia dar resultados diferentes em outro. Era o caos numérico.
Para resolver isso, surgiu em 1985 o padrão IEEE 754, que unificou a linguagem matemática dos processadores. Esse padrão define vários “tamanhos” de números, adequados para diferentes necessidades:
- Half Precision (16 bits): Muito usado hoje em placas de vídeo (GPUs) para Inteligência Artificial e Deep Learning, onde a velocidade importa mais que a precisão extrema.
- Single Precision (32 bits): O clássico
floatda linguagem C. É o padrão para geometria em jogos 3D, áudio digital e processamento gráfico. - Double Precision (64 bits): O padrão do
floatem Python, JavaScript e Ruby. É fundamental para simulações científicas e engenharia civil, onde o erro acumulado de 32 bits seria perigoso. - Quadruple Precision (128 bits): Usado em nichos extremos, como balística interplanetária ou criptografia avançada.
Nossa Escolha: O Modelo de 32 Bits #
Neste artigo, vamos focar na estrutura de Single Precision (32 bits). “Mas se eu uso Python (64 bits), por que aprender o de 32?”
A resposta é didática: A lógica é idêntica. Todos eles funcionam dividindo a memória em três campos (Sinal, Expoente, Mantissa). A diferença é apenas a quantidade de bits em cada campo.
Escolhemos 32 bits porque é o equilíbrio perfeito: complexo o suficiente para mostrar todos os problemas reais (como erros de arredondamento), mas curto o suficiente para conseguirmos escrever os binários na tela sem encher 3 linhas de zeros e uns.
Comparativo: 32 bits vs 64 bits #
Embora a estrutura lógica seja a mesma, os tamanhos dos campos mudam para acomodar maior precisão e alcance no padrão de 64 bits (usado pelo Python/JS).
| Característica | Single Precision (32 bits) | Double Precision (64 bits) |
|---|---|---|
| Uso Comum | C float, GPUs, Jogos, Áudio |
Python float, JS Number, C double |
| Tamanho Total | 32 bits | 64 bits |
| Sinal | 1 bit | 1 bit |
| Expoente | 8 bits | 11 bits |
| Mantissa | 23 bits | 52 bits |
| Viés (Bias) | 127 | 1023 |
| Precisão Decimal | ~7 dígitos significativos | ~15-17 dígitos significativos |
| Alcance (Range) | \( \approx 10^{-38} \) a \( 10^{38} \) | \( \approx 10^{-308} \) a \( 10^{308} \) |
Lembre-se: quando você cria um x = 1.5 em Python, você está usando a coluna
da direita (64 bits). Isso significa que você tem muito mais precisão do que
os exemplos manuais de 32 bits que faremos abaixo, mas as regras de erro de
arredondamento e estrutura são idênticas.
Abrindo o Capô #
Imagine então um bloco de memória de 32 bits.
Diferente dos números inteiros (int), onde todos os bits trabalham juntos
somando potências de 2 para formar um único valor, aqui a memória é fatiada em
três campos com funções completamente distintas. É como se tivéssemos três
mini-variáveis empacotadas dentro de uma só.
A divisão oficial do IEEE 754 para 32 bits é:
| Campo | Tamanho | Bits (Posição) | O que faz? |
|---|---|---|---|
| Sinal | 1 bit | 31 | Define se é positivo (+) ou negativo (-). |
| Expoente | 8 bits | 30 a 23 | Define a magnitude (escala). |
| Mantissa | 23 bits | 22 a 0 | Define a precisão (dígitos). |
Visualmente, os 32 bits são organizados assim:
[S] [EEEEEEEE] [MMMMMMMMMMMMMMMMMMMMMMM]
1 8 23
Ou, com ainda mais detalhes, de acordo com a figura a seguir. Os conceitos de normalização e valores especiais serão explicados adiante no artigo.

Vamos dissecar cada parte dessa “régua”.
O Bit de Sinal (1 bit) #
O bit mais à esquerda (MSB - Most Significant Bit) é o dono do sinal.
- 0 indica um número Positivo.
- 1 indica um número Negativo.
Nos inteiros, usamos o Complemento de Dois, onde o bit de sinal tem um “peso negativo” e altera a aritmética de todo o número. No Ponto Flutuante, o sistema é Sinal-Magnitude. O bit de sinal é apenas uma flag (“bandeira”, indicador). Se você inverter apenas esse bit, o valor numérico permanece idêntico, mudando apenas de \(+x\) para \(-x\). Isso torna muito fácil para o processador mudar o sinal de um número: basta inverter um único bit.
O Expoente (8 bits) #
Os próximos 8 bits definem a Escala do número. É aqui que o computador decide onde a vírgula vai estar.
- Pense nisso como o expoente da notação científica (\(10^5\) ou \(10^{-3}\)).
- Como são 8 bits, podemos ter valores de \(0\) a \(255\).
- Isso nos permite representar magnitudes gigantescas (até aprox. \(3.4 \times 10^{38}\)) ou minúsculas (até \(1.1 \times 10^{-38}\)).
O valor armazenado aqui não é o expoente “real” da matemática, mas sim um valor “enviesado”. Veremos esse truque técnico adiante.
A Mantissa (23 bits) #
Os 23 bits restantes (à direita) são a Mantissa (ou Significando). É aqui que mora a Precisão.
- A Mantissa contém os dígitos significativos do número.
- Enquanto o Expoente diz “quão grande” o número é, a Mantissa diz “quão exato” ele é.
- Se você precisa diferenciar \(1.000001\) de \(1.000002\), é a Mantissa que tem que ter bits suficientes para guardar essa diferença.
A Fórmula do Hardware #
Quando o computador lê esses 32 bits, ele não vê o número de uma vez. Ele joga os pedaços na seguinte equação para reconstruir o valor real:
$$ Valor = (-1)^{Sinal} \times (1.Mantissa) \times 2^{Expoente - Viés} $$Pode parecer intimidante, mas é apenas a versão matemática de dizer: “Pegue o sinal, multiplique pelo corpo do número (1 vírgula alguma coisa) e desloque a vírgula tantas casas para a esquerda ou direita”.
O conceito mais difícil de aceitar para quem vem dos Inteiros é que o valor de um bit no Ponto Flutuante depende do vizinho (o Expoente).
- Em um inteiro de 32 bits, o bit na posição 0 sempre vale \(1\) unidade. O bit na posição 5 sempre vale \(32\) unidades.
- Em um float, o último bit da Mantissa pode valer \(0.0000001\) se o Expoente for pequeno, ou pode valer \(1.000.000\) se o Expoente for gigante.
É por isso que dizemos que floats têm precisão relativa, não absoluta. O “tamanho” do bit muda conforme o tamanho do número.
O Truque do “Hidden Bit” (Bit Implícito) e Normalização #
Se você olhar para a estrutura de 32 bits, verá que reservamos 23 bits para a Mantissa. Isso nos dá uma certa precisão. Mas os engenheiros que desenharam o padrão IEEE 754 perceberam que poderiam ganhar um bit extra “de graça” usando uma propriedade matemática do sistema binário.
Para entender isso, precisamos falar sobre Normalização.
O Que é um Número Normalizado? #
Na notação científica decimal, podemos escrever o número 300 de várias formas:
- \(300 \times 10^0\)
- \(30.0 \times 10^1\)
- \(0.3 \times 10^3\)
Embora matematicamente iguais, essa bagunça atrapalha o computador (como comparar números se eles têm formatos diferentes?). Por isso, existe uma regra: a notação científica é Normalizada quando existe apenas um dígito não-nulo antes da vírgula. No caso acima, a forma normalizada é \(3.0 \times 10^2\).
Agora, vamos para o Binário. Em binário, os únicos dígitos disponíveis são \(0\) e \(1\). Se aplicarmos a regra da normalização (“o primeiro dígito não pode ser zero”), qual é a única opção que sobra? O número 1.
Isso significa que, em qualquer número binário normalizado (seja ele gigantesco ou microscópico), a forma sempre será:
\[ 1.\text{alguma\_coisa} \times 2^{\text{expoente}} \]Exemplos:
- \(1.001 \times 2^5\)
- \(1.1101 \times 2^{-3}\)
A regra do “Bit 1 Implícito” vale para a vasta maioria dos números, chamados de Números Normalizados. Porém, existe uma exceção importante para números extremamente minúsculos (próximos de zero), onde essa regra é desligada para evitar erros de cálculo. Chamamos esses números de Números Subnormais, e eles terão uma seção exclusiva mais à frente. Por enquanto, assuma que a regra vale sempre.
A Sacada de Engenharia: O Bit Fantasma #
Aqui entra a genialidade do padrão. Se todo número normalizado começa obrigatoriamente com \(1.\), por que desperdiçar um precioso bit de memória para armazenar esse “1”? Ele é redundante. O computador já sabe que ele está lá!
O IEEE 754 descarta esse “1” inicial antes de gravar na memória e armazena apenas o que vem depois da vírgula (a parte fracionária).
Quando o processador lê o número de volta para fazer uma conta, ele coloca esse bit de volta, colocando-o automaticamente na frente da mantissa. É o chamado Hidden Bit (Bit Escondido) ou Bit Implícito.
O Ganho Real: Embora tenhamos apenas 23 bits físicos na memória para a Mantissa, a precisão efetiva do sistema é de 24 bits (23 armazenados + 1 implícito). Isso reduz o erro relativo dos cálculos pela metade sem custar nenhum hardware extra de armazenamento.
Além de economizar espaço, a normalização resolve um problema crítico de arquitetura: a Unicidade da Representação.
Imagine se o computador permitisse armazenar o mesmo valor como
\(1.0 \times 2^1\) e \(0.1 \times 2^2\). Para verificar se A == B, o
processador teria que
fazer malabarismos matemáticos complexos para alinhar os expoentes antes de
comparar.
Ao forçar que os números sejam normalizados, garantimos que cada número real (dentro do alcance normalizado) tenha apenas uma representação binária padrão. Isso evita a ambiguidade de ter o mesmo número escrito de formas diferentes na memória, o que facilita muito a vida do hardware. Nota: As exceções a essa unicidade são o Zero (que tem sinal) e o NaN.
Contudo, essa regra cria uma pergunta óbvia: “Se o número sempre começa com 1, como representamos o Zero?” A resposta é que o Zero é uma exceção especial às regras, que veremos na seção sobre “Valores Especiais”. Por enquanto, assuma que todo número tem esse “1.” invisível na frente.
O Expoente e o Viés (Bias) #
Já sabemos como representar o sinal (1 bit) e o corpo do número (Mantissa). Agora falta resolver a escala.
Na notação científica, o expoente é quem define se o número é gigantesco (\(2^{100}\)) ou microscópico (\(2^{-100}\)). Como temos 8 bits reservados para o expoente, podemos representar \(256\) valores diferentes.
O problema óbvio é: precisamos de números negativos. Sem expoentes negativos, só conseguiríamos representar números maiores que 1. Não conseguiríamos representar \(0.5\) (\(2^{-1}\)) ou o tamanho de um elétron.
No artigo sobre números inteiros, aprendemos que a solução padrão para guardar negativos é o Complemento de Dois. Porém, o padrão IEEE 754 não usa Complemento de Dois para o expoente. Ele usa um sistema chamado Expoente com Viés (Biased Exponent).
A Matemática do Viés #
Em vez de sacrificar um bit para sinal, o padrão define um valor de “viés” (bias) que é somado ao expoente real antes de guardá-lo na memória.
Para precisão simples (32 bits), o viés é 127.
A fórmula de armazenamento é:
$$ E_{\text{armazenado}} = E_{\text{real}} + 127 $$Isso transforma o intervalo de expoentes em uma escala de inteiros positivos sem sinal (Unsigned):
- Se você quer um expoente Zero (\(2^0\)): Guarda \(0 + 127 = \mathbf{127}\) (\(01111111_2\)).
- Se você quer um expoente Positivo (\(2^{10}\)): Guarda \(10 + 127 = \mathbf{137}\).
- Se você quer um expoente Negativo (\(2^{-10}\)): Guarda \(-10 + 127 = \mathbf{117}\).
Com isso, o expoente armazenado varia de \(1\) a \(254\) (lembrando que \(0\) e \(255\) são reservados para casos especiais, como veremos adiante).
Por que complicar? #
Você deve estar se perguntando: “Por que não usaram o Complemento de Dois, que o computador já sabe fazer nativamente?”
A resposta está na velocidade de comparação. Computadores frequentemente precisam responder: “O número A é maior que o número B?”.
Se usássemos Complemento de Dois, números com expoentes negativos (começando com bit 1) pareceriam aritmeticamente “maiores” que números com expoentes positivos (começando com bit 0) se fossem lidos como inteiros comuns.
O uso do Viés preserva a ordem lexicográfica dos números de ponto flutuante.
Graças ao Viés, o expoente de um número pequeno (ex: \(2^{-10} \to 117\)) gera uma sequência de bits menor que o expoente de um número grande (ex: \(2^{10} \to 137\)).
O Resultado Prático: Isso permite comparar dois números de Ponto Flutuante positivos usando os mesmos circuitos simples e rápidos que comparam Inteiros Sem Sinal. Para números negativos, a lógica precisa apenas de um ajuste simples de sinal, mas a estrutura do expoente continua facilitando drasticamente a ordenação.
Mão na Massa: Convertendo um Número (-12.5) #
Teoria é bom, mas é na conversão manual que realmente entendemos como as engrenagens se encaixam. Vamos converter o número decimal -12.5 para o padrão IEEE 754 de 32 bits, passo a passo.
Nosso objetivo é preencher os três campos: [Sinal] [Expoente] [Mantissa].
Passo 1: O Sinal #
O número é negativo (-12.5). Logo, o Bit de Sinal é 1.
Passo 2: Binário “Bruto” #
Primeiro, esquecemos o sinal e convertemos a magnitude (\(12.5\)) para binário usando o método de Ponto Fixo que já conhecemos.
- Parte Inteira (12): \(1100_2\)
- Parte Fracionária (0.5): \(0.1_2\) (pois \(1 \times 2^{-1} = 0.5\))
Juntando as partes:
\[ 12.5_{10} = 1100.1_2 \]Passo 3: Normalização (Notação Científica) #
Agora precisamos mover a vírgula para a esquerda até que reste apenas um dígito \(1\) antes dela. Para transformar \(1100.1\) em \(1.1001\), precisamos mover a vírgula 3 casas para a esquerda.
\[ 1100.1 = 1.1001 \times 2^3 \]Daqui extraímos duas informações vitais:
- Expoente Real: \(3\)
- Mantissa Bruta: \(1.1001\)
Passo 4: O Expoente com Viés #
O expoente real é \(3\). Mas lembre-se da Seção 4: não guardamos o \(3\). Guardamos o \(3 + \text{Viés}\). Para precisão simples, o viés é \(127\).
\[ \text{Expoente Armazenado} = 3 + 127 = 130 \]Agora convertemos \(130\) para binário (8 bits). Sabemos que \(128\) é \(10000000\), então \(130\) (\(128 + 2\)) é: 10000010
Passo 5: A Mantissa Final (O Bit Escondido) #
Olhamos para a mantissa normalizada do Passo 3: \(\mathbf{1}.1001\). Lembre-se da Seção 3: o “1.” inicial é implícito (Hidden Bit). Nós o descartamos e guardamos apenas o que vem depois da vírgula.
- Bits para guardar:
1001 - Como temos 23 bits de espaço, preenchemos o restante com zeros à direita.
Mantissa Armazenada: 10010000000000000000000
Passo 6: Montagem Final #
Agora basta colar as três partes lado a lado:
- Sinal (1 bit):
1 - Expoente (8 bits):
10000010 - Mantissa (23 bits):
10010000000000000000000
O número binário de 32 bits é:
1 10000010 10010000000000000000000
Passo Bônus: Hexadecimal #
Ler 32 bits é doloroso. Programadores preferem Hexadecimal. Vamos agrupar os bits de 4 em 4 e converter:
$$ \begin{array}{c c c c c c c c} 1100 & 0001 & 0100 & 1000 & 0000 & 0000 & 0000 & 0000 \\ \downarrow & \downarrow & \downarrow & \downarrow & \downarrow & \downarrow & \downarrow & \downarrow \\ \mathbf{C} & \mathbf{1} & \mathbf{4} & \mathbf{8} & \mathbf{0} & \mathbf{0} & \mathbf{0} & \mathbf{0} \end{array} $$Portanto, se você inspecionar a memória do seu computador quando ele guarda -12.5 num float, você encontrará o valor Hexadecimal 0xC1480000.
Você pode confirmar nossa conta usando o módulo struct do Python, que permite ver os bytes brutos da memória:
import struct
# 'f' indica float (32 bits), pack converte para bytes
# unpack converte bytes para inteiro para vermos o hex
bits = struct.unpack('I', struct.pack('f', -12.5))[0]
print(hex(bits))
# Saída: 0xc1480000A matemática funciona!
Outro exemplo: Decimal para IEEE 754 #
O exemplo anterior foi “limpo” porque o número tinha uma representação binária exata. Agora, vamos lidar com um número decimal que não pode ser representado exatamente em binário, para ver como o processo lida com frações.
Alvo: Converter o número \(0.085\) para o formato de precisão simples (32 bits).
Vamos seguir um passo a passo que cobre todos os detalhes:
1. Determinar o Sinal Como \(0.085\) é um número positivo, o bit de sinal é 0.
2. Converter para Notação Científica Binária Precisamos escrever o número no formato \(1.f \times 2^e\), onde \(1 \le 1.f < 2\). Usamos multiplicações sucessivas por 2 — o mesmo método do artigo anterior — até o produto ultrapassar 1, o que identifica onde o número se normaliza:
- \(0.085 \times 2 = 0.17\) → Bit 0
- \(0.17 \times 2 = 0.34\) → Bit 0
- \(0.34 \times 2 = 0.68\) → Bit 0
- \(0.68 \times 2 = 1.36\) → Bit 1 \(\leftarrow\) Primeiro
1encontrado (sobrou 0.36)
O primeiro 1 apareceu na 4ª multiplicação, o que significa que a vírgula recua 4 posições ao normalizar. Portanto, o Expoente Real é \(-4\):
A mantissa será derivada dos bits que seguem o 1., começando pela fração restante \(0.36\).
3. Calcular o Expoente com Viés O padrão usa um viés de 127.
\[ \text{Expoente Armazenado} = -4 + 127 = 123 \]Convertendo 123 para binário (8 bits): \(123 = 01111011_2\)
4. Converter a Mantissa para Binário Precisamos converter a fração \(0.36\) para binário multiplicando sucessivamente por 2 até preencher 23 bits (mais um bit extra para arredondamento):
- \(0.36 \times 2 = 0.72 \to\) Bit 0
- \(0.72 \times 2 = 1.44 \to\) Bit 1 (sobrou 0.44)
- \(0.44 \times 2 = 0.88 \to\) Bit 0
- \(0.88 \times 2 = 1.76 \to\) Bit 1 (sobrou 0.76)
- \(0.76 \times 2 = 1.52 \to\) Bit 1 (sobrou 0.52)
- \(0.52 \times 2 = 1.04 \to\) Bit 1 (sobrou 0.04)
- \(0.04 \times 2 = 0.08 \to\) Bit 0
- \(0.08 \times 2 = 0.16 \to\) Bit 0
- \(0.16 \times 2 = 0.32 \to\) Bit 0
- \(0.32 \times 2 = 0.64 \to\) Bit 0
- \(0.64 \times 2 = 1.28 \to\) Bit 1 (sobrou 0.28)
- \(0.28 \times 2 = 0.56 \to\) Bit 0
- \(0.56 \times 2 = 1.12 \to\) Bit 1 (sobrou 0.12)
- \(0.12 \times 2 = 0.24 \to\) Bit 0
- \(0.24 \times 2 = 0.48 \to\) Bit 0
- \(0.48 \times 2 = 0.96 \to\) Bit 0
- \(0.96 \times 2 = 1.92 \to\) Bit 1 (sobrou 0.92)
- \(0.92 \times 2 = 1.84 \to\) Bit 1 (sobrou 0.84)
- \(0.84 \times 2 = 1.68 \to\) Bit 1 (sobrou 0.68)
- \(0.68 \times 2 = 1.36 \to\) Bit 1 (sobrou 0.36 — padrão reinicia aqui)
- \(0.36 \times 2 = 0.72 \to\) Bit 0 (bit 21)
- \(0.72 \times 2 = 1.44 \to\) Bit 1 (bit 22)
- \(0.44 \times 2 = 0.88 \to\) Bit 0 (bit 23)
- \(0.88 \times 2 = 1.76 \to\) Bit 1 (bit 24 — usado para arredondamento)
A fração \(0.36\) é periódica em binário com período 20. O bit 24 de arredondamento é 1, logo o
bit 23 é arredondado de 0 para 1 (regra Round to Nearest Even). Mantissa final de 23 bits:
01011100001010001111011
Resultado Final (Bits):
Juntando tudo: Sinal 0, Expoente 01111011, Mantissa 01011100001010001111011. Logo:
0 01111011 01011100001010001111011
Exemplo: IEEE 754 para Decimal #
Agora que sabemos escrever na memória, vamos fazer o processo inverso: a Engenharia Reversa. Como lemos um padrão de bits desconhecido?
Alvo: Descobrir que número decimal está guardado no padrão de bits:
11000000110110011001100110011010
Seguindo a engenharia reversa do passo a passo da seção anterior:
1. Fatiar os Bits Separamos o binário nos três campos do padrão:
- Sinal (1 bit):
1 - Expoente (8 bits):
10000001 - Mantissa (23 bits):
10110011001100110011010
2. Analisar o Sinal
O bit é 1, logo o número é Negativo.
3. Decodificar o Expoente
O valor binário 10000001 equivale a \(128 + 1 = 129\) em decimal.
Para achar o expoente real, subtraímos o viés (127):
Isso significa que vamos multiplicar a mantissa por \(2^2\).
4. Decodificar a Mantissa
A mantissa armazenada é 10110011.... Devemos lembrar que existe um 1. implícito na frente e que cada bit representa uma potência negativa de 2 (\(2^{-1}, 2^{-2}, 2^{-3}...\)):
(Nota: A fração 0.10110011... é uma dízima periódica em binário — o padrão 0110 repete-se a cada 4 bits a partir do segundo bit. Somando todos os termos da série, a fração converge para exatamente \(0.7\), confirmando a mantissa \(1.6875 + 0.0125 = 1.7\).)
5. Cálculo Final Agora aplicamos a fórmula completa:
\[ \begin{aligned} \text{Número} &\approx (-1)^{\text{sinal}} \times (1.\text{mantissa}) \times 2^{\text{expoente}} \\ &\approx -1 \times 1.7 \times 2^2 \\ &\approx -1.7 \times 4 \\ &\approx -6.8 \end{aligned} \]Portanto, aqueles bits representam o valor decimal aproximado -6.8. O valor é aproximado devido à natureza da representação binária de frações.
Arredondamento e o “Cancelamento Catastrófico” #
Até agora, falamos sobre como guardar números. Mas o computador foi feito para calcular. E é nas operações de soma e subtração que ocorrem os erros mais sutis e perigosos.
O primeiro ponto a entender é que o padrão IEEE 754 não “corta” simplesmente os números que não cabem na mantissa. Ele arredonda. Mas ele faz isso de um jeito estatisticamente inteligente.
Round to Nearest Even (Arredondar para o Par) #
Na escola, aprendemos algumas variações de arredondamento (sempre para cima, sempre para baixo, análise do dígito seguinte), mas para computadores, essas estratégias podem introduzir um viés sistemático.
Para evitar isso, o padrão usa o modo Round to Nearest Even (Arredondar para o Par Mais Próximo). A regra é simples: se o número estiver exatamente no meio (terminado em …5), arredondamos para o número Par mais próximo.
- \(1.5 \to 2\) (Arredondou para cima)
- \(2.5 \to 2\) (Arredondou para baixo)
- \(3.5 \to 4\) (Arredondou para cima)
- \(4.5 \to 4\) (Arredondou para baixo)
Dessa forma, em uma longa sequência de cálculos, os arredondamentos para cima e para baixo tendem a se anular, mantendo a média estatística dos dados muito mais fiel à realidade.
O padrão IEEE 754 possui outros modos (como arredondar sempre para cima ou para baixo), usados em Aritmética de Intervalos para garantir que o resultado real esteja dentro de uma margem de segurança conhecida. Mas, para o uso geral, o “Nearest Even” é o padrão.
Para visualizar a diferença entre o padrão e as alternativas, observe a tabela abaixo aplicada a valores monetários. Preste atenção especial em como cada modo lida com o “meio termo” (x.50) e com os números negativos.
| Modo de Arredondamento | $1.40 | $1.60 | $1.50 | $2.50 | $-1.50 |
|---|---|---|---|---|---|
| Round-to-even | $1 | $2 | $2 | $2 | $-2 |
| Round-toward-zero | $1 | $1 | $1 | $2 | $-1 |
| Round-down | $1 | $1 | $1 | $2 | $-2 |
| Round-up | $2 | $2 | $2 | $3 | $-1 |
O que observar nesta tabela:
- O “Empate” do .50 (Round-to-even): Veja as colunas $1.50 e $2.50. Ambos foram arredondados para $2. Isso confirma a regra do “Par Mais Próximo”. Se usássemos o arredondamento escolar tradicional, o $1.50 iria para $2, mas o $2.50 iria para $3, criando um viés de aumento na média total dos dados.
- O Truncamento (Round-toward-zero): Este modo apenas corta a parte
decimal. É o comportamento padrão quando você converte
int(1.9)em muitas linguagens: o resultado é 1, não 2. - Direção na Reta Numérica (Down vs Up): Cuidado com os negativos!
- Round-down sempre vai para a esquerda na reta numérica (em direção ao \(-\infty\)). Por isso, \(-1.50\) desce para \(-2\). É o comportamento de funções como
math.floor()em Python. - Round-up sempre vai para a direita na reta numérica (em direção ao \(+\infty\)). Por isso, \(-1.50\) sobe para \(-1\). É o comportamento de funções como
math.ceil()em Python.
- Round-down sempre vai para a esquerda na reta numérica (em direção ao \(-\infty\)). Por isso, \(-1.50\) desce para \(-2\). É o comportamento de funções como
Podemos ver essas regras em ação usando um script Python simples. Note especialmente a
diferença entre int e floor para números negativos, e como o round arredonda
para o par mais próximo.
import math
valores = [1.5, 2.5, -1.5]
print(f"{'Num':<6} {'int':<6} {'floor':<6} {'ceil':<6} {'round':<6}")
print("-" * 40)
for n in valores:
# int() = Truncamento
# floor() = Arredondar para baixo (Esquerda)
# ceil() = Arredondar para cima (Direita)
# round() = Nearest Even (Par mais próximo)
print(f"{n:<6} {int(n):<6} {math.floor(n):<6} {math.ceil(n):<6} {round(n):<6}")Saída:
Num int floor ceil round
----------------------------------------
1.5 1 1 2 2
2.5 2 2 3 2
-1.5 -1 -2 -1 -2O Perigo: Cancelamento Catastrófico #
O arredondamento gera pequenos erros na última casa decimal. Geralmente, isso é aceitável. O problema real acontece quando esses pequenos erros são promovidos a protagonistas. Isso se chama Cancelamento Catastrófico.
Isso ocorre quando subtraímos dois números muito próximos.
Imagine que temos dois valores que deveriam ser exatos, mas sofreram um minúsculo erro de arredondamento na 7ª casa decimal:
$$ \begin{array}{r} x = 1.000000\mathbf{5} \\ y = 1.000000\mathbf{4} \end{array} $$Ao fazer \(x - y\):
$$ 0.0000001 $$O computador agora precisa Normalizar esse resultado (lembra da regra do “1 na frente”?). Ele vai mover a vírgula 7 casas para a direita para transformar isso em \(1.0 \times 10^{-7}\).
Onde está o problema? Originalmente, \(x\) e \(y\) tinham 7 dígitos de precisão confiável. O resultado da subtração tem apenas 1 dígito de precisão. Os bits “bons” (o \(1.000...\)) se cancelaram mutuamente. O que sobrou foi apenas a “sujeira” ou o “ruído” que estava no final dos números. Se aquele \(...5\) e \(...4\) fossem frutos de arredondamento anterior (lixo), agora o seu resultado final é 100% lixo, mas o computador vai tratá-lo como um número válido e preciso.
Vamos usar a fórmula quadrática (Bhaskara) para ilustrar isso. Se você calcular \(-b + \sqrt{b^2 - 4ac}\) e acontecer de \(b^2\) ser muito próximo de \(4ac\), a raiz quadrada será muito próxima de \(b\).
Ao fazer a subtração final \(-b + \text{quase\_b}\), ocorre o cancelamento catastrófico. O resultado pode estar errado em ordens de magnitude. Solução: Matemáticos e engenheiros reescrevem essas fórmulas (usando racionalização ou identidades trigonométricas) para evitar subtrações de valores próximos.
Isso nos ensina a lição de ouro do Ponto Flutuante: Nunca confie cegamente na subtração de números parecidos.
Valores especiais: Infinity, NaN e Zero com Sinal #
Até agora, assumimos que todo padrão de bits resulta em um número válido. Mas o
que acontece se o expoente for todo preenchido com uns (11111111) ou todo
com zeros (00000000)?
O padrão IEEE 754 reserva esses valores extremos de expoente para representar estados especiais. Eles não são números normais, mas conceitos matemáticos e erros que precisam ser tratados sem travar o computador.
O Zero tem Sinal (+0 e -0) #
Nos números inteiros, zero é apenas zero. Mas no Ponto Flutuante, o Zero é um estado muito específico definido por:
- Expoente: Todos os bits
0(00000000). - Mantissa: Todos os bits
0(vazia).
Se o expoente for zero, mas a mantissa tiver algum bit 1, não estamos olhando para o Zero, mas sim para um número Subnormal, que veremos mais à frente.
Isso cria uma situação aparentemente bizarra: existe o Zero
Positivo (0 00000000 000...) e o Zero Negativo (1 00000000 000...).
Para a maioria das comparações lógicas no seu código, +0 == -0 retorna
Verdadeiro. O computador finge que são iguais.
Mas por que o Zero Negativo existe? O zero com sinal é vital para preservar identidades matemáticas e resolver problemas complexos (como cortes de ramo na análise complexa). O exemplo mais simples é a função \(1/x\).
- Se \(x\) se aproxima de zero pelo lado positivo (\(+0\)), o resultado deve ser \(+\infty\).
- Se \(x\) se aproxima de zero pelo lado negativo (\(-0\)), o resultado deve ser \(-\infty\).
Sem o zero negativo, perderíamos a informação de sinal em operações que resultam em “underflow” (números tão pequenos que viram zero), quebrando a identidade matemática \(1/(1/x) = x\).
Infinity (Infinito) #
O que acontece se você multiplicar \(10^{30} \times 10^{30}\)? O resultado (\(10^{60}\)) é maior do que o valor máximo que cabe em 32 bits. Em números inteiros, isso causaria um Overflow silencioso e o número “daria a volta”, virando negativo ou zero. Um desastre para cálculos físicos.
No IEEE 754, se o número explode o limite, ele vira Infinity.
- Representação: Expoente
11111111(255) e Mantissa0. - Comportamento: O Infinito permite que a matemática continue. Dependendo da
configuração da linguagem ou do processador, \(5 \div 0\) pode gerar uma exceção
(erro) ou retornar silenciosamente \(\infty\). No padrão IEEE 754 puro, o
resultado é definido:
- \(\infty + 1000 = \infty\)
- \(\infty + \infty = \infty\)
- \(5 \div 0 = \infty\) (Gera uma flag de “Divisão por Zero”, mas segue o cálculo)
Essa decisão de design permite que o software continue rodando mesmo após um estouro, em vez de abortar a execução imediatamente.
NaN (Not a Number) #
E se tentarmos fazer algo matematicamente ilegal?
- Dividir \(0 \div 0\).
- Calcular \(\sqrt{-1}\) (no domínio dos reais).
- Subtrair \(\infty - \infty\).
O resultado não é um número. É um NaN (Not a Number).
- Representação: Expoente
11111111(255) e Mantissa diferente de0.
O NaN se propaga. Qualquer conta que você faça com um NaN resulta em NaN.
\[ 50 + \text{NaN} = \text{NaN} \]O NaN tem uma propriedade única: ele não é igual a ele mesmo.
Embora você possa ver códigos usando x != x para detectar isso, a forma
robusta e recomendada em produção é usar a biblioteca padrão:
import math
x = float('nan')
# O jeito certo:
if math.isnan(x):
print("É um NaN!")O objetivo do NaN é permitir debug. Em vez de o programa fechar (crash) no meio de um cálculo longo, ele propaga o NaN até o final, permitindo rastrear a origem do erro.
O Limbo dos Subnormais (Gradual Underflow) #
Imagine que você está diminuindo a intensidade de uma lâmpada (dimmer). Você vai baixando a luz suavemente até que… clique. Ela apaga de vez.
Nos primórdios da computação, os números funcionavam assim. O menor número “normal” possível em 32 bits é \(1.0 \times 2^{-126}\) (aproximadamente \(1.17 \times 10^{-38}\)). Se você tentasse dividir esse número por 2, o resultado seria menor do que o mínimo representável. O computador arredondava direto para Zero.
Isso era chamado de Underflow Abrupto (Flush-to-Zero). O problema disso é que a subtração \(A - B\) podia resultar em Zero, mesmo que \(A\) e \(B\) fossem diferentes! Isso quebrava algoritmos matemáticos e causava instabilidade em softwares de engenharia.
A Solução: Desligando o Bit Implícito #
Para resolver isso, o IEEE 754 criou uma “zona de amortecimento” entre o menor número normal e o zero absoluto. São os Números Subnormais (ou Denormalized).
A regra é ativada automaticamente quando o Expoente é 00000000. Quando o processador vê o expoente zerado, ele muda o modo de operação:
- O Bit Escondido vira 0: Em vez de assumir \(1.mantissa\), o computador assume \(0.mantissa\).
- O Expoente fixa em -126: Mesmo que os bits sejam zero, o valor do expoente não desce para -127 (o que seria lógico pelo viés), mas trava em -126 para manter a continuidade suave com os números normais.
A fórmula muda para:
\[ Valor = (-1)^{Sinal} \times (0.Mantissa) \times 2^{-126} \]O Efeito Prático: Gradual Underflow #
Isso permite que os números vão “desaparecendo” aos poucos, bit por bit, em vez de sumirem de repente.
- Menor Normal: \(1.000...00 \times 2^{-126}\)
- Maior Subnormal: \(0.111...11 \times 2^{-126}\) (Um pouco menor que o de cima)
- …
- Menor Subnormal: \(0.000...01 \times 2^{-126}\) (Apenas o último bit ligado)
- Zero: \(0.000...00 \times 2^{-126}\)
O Custo da Subnormalidade #
Nada vem de graça. Ao usar números subnormais, pagamos um preço: Perda de Precisão.
Lembre-se que a notação científica serve para manter a precisão sempre alta. Mas nos subnormais, começamos a encher o início do número com zeros (\(0.00001...\)). Esses zeros ocupam espaço na Mantissa e reduzem a quantidade de “bits úteis” (significativos). Quanto menor o número subnormal, menos confiável ele é. Mas, segundo o padrão, é melhor ter um número com pouca precisão do que um zero falso.
Calcular com subnormais é difícil. Em muitos processadores mais antigos (e
alguns modernos), operações com subnormais são tratadas via software
(microcódigo) em vez de hardware direto, o que pode ser centenas de vezes mais
lento.
Por isso, alguns compiladores de alta performance possuem flags como
-ffast-math que reativam o “Flush-to-Zero”, sacrificando a precisão matemática
em troca de velocidade bruta em jogos ou simulações.
O Erro de Precisão (0.1 + 0.2 != 0.3) #
Se você abrir o console do Python (ou JavaScript, C, Java…) e digitar:
>>> 0.1 + 0.2 == 0.3
FalseA resposta será False. E se você pedir para ver o resultado da soma:
>>> 0.1 + 0.2
0.30000000000000004De onde veio esse ...4 no final? O computador errou uma conta de primário?
Não. Ele fez a conta perfeitamente, dentro dos limites do sistema binário.
O Problema das Dízimas Periódicas #
Para entender isso, precisamos voltar à escola. Em decimal (base 10), sabemos que algumas frações não podem ser representadas de forma finita.
Temos uma dízima periódica. Se você somar \(0.333 + 0.333 + 0.333\), o resultado é \(0.999\), não \(1.0\). Houve perda de precisão porque cortamos a dízima.
O mesmo acontece em binário, mas com números diferentes. Em binário, só conseguimos representar finitamente frações que são somas de potências de 2 (como \(0.5, 0.25, 0.125\)).
O caso do 0.1: Vamos tentar converter \(0.1\) decimal para binário:
- \(0.1 \times 2 = 0.2\) \(\to\) Dígito 0
- \(0.2 \times 2 = 0.4\) \(\to\) Dígito 0
- \(0.4 \times 2 = 0.8\) \(\to\) Dígito 0
- \(0.8 \times 2 = \mathbf{1}.6\) \(\to\) Dígito 1 (Sobra 0.6)
- \(0.6 \times 2 = \mathbf{1}.2\) \(\to\) Dígito 1 (Sobra 0.2)
- Opa! Voltamos para 0.2 (Passo 2).
O ciclo se repete infinitamente: \(0.00011001100110011...\). O número \(0.1\) é uma dízima periódica em binário.
A Guilhotina dos 23 Bits #
Como vimos nas seções anteriores, o padrão IEEE 754 (Single Precision) tem apenas 23 bits na mantissa (mais o bit escondido). Ele não tem espaço infinito para guardar essa dízima. Em algum momento, o computador precisa passar a “tesoura” e parar de gravar.
Ao fazer isso, ele não está guardando \(0.1\). Ele está guardando um número extremamente próximo, mas ligeiramente menor ou maior (dependendo do arredondamento).
- O computador guarda o “0.1” como algo próximo de: \(0.100000001490116...\)
- O computador guarda o “0.2” como algo próximo de: \(0.200000002980232...\)
Ao somar essas duas aproximações, os erros se acumulam e o resultado ultrapassa a margem de erro que o computador usa para exibir “0.3” na tela, revelando o “lixo” no final: \(0.30000000000000004\).
== com Floats
Espremer infinitos números reais em um número
finito de bits exige aproximação. Portanto, igualdade estrita (==) quase
nunca funciona para resultados calculados.
A Forma Correta:
Verifique se os números estão “perto o suficiente” um do outro, usando uma
margem de erro minúscula (chamada de tolerância).
import math
# Forma correta de comparar
a = 0.1 + 0.2
b = 0.3
if math.isclose(a, b):
print("São iguais (para fins práticos)")Na função isclose, você pode ajustar a tolerância conforme a precisão necessária
para o seu domínio específico. Segue um exemplo em Python:
import math
a = 0.1 + 0.2
b = 0.3
# Verifica se 'a' e 'b' são próximos o suficiente
if math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
print("São iguais (para fins práticos)")Nesse exemplo, rel_tol=1e-9 define a tolerância relativa, permitindo que a e
b sejam considerados iguais se estiverem dentro de uma diferença de
\(10^{-9}\) vezes o valor de b. O parâmetro abs_tol=0.0 indica que não há
tolerância absoluta adicional. Ajuste esses valores conforme a precisão
necessária para o seu caso de uso.
Para Dinheiro:
Se você está lidando com dinheiro, centavos importam e erros de arredondamento
são inaceitáveis (e ilegais em contabilidade). Nunca use Float para
dinheiro. Use tipos Decimal (em Python) ou BigDecimal (em Java), que
trabalham com base 10 e não sofrem desse problema.
Conclusão #
Ao longo desta jornada, saímos da aritmética simples de contar laranjas com inteiros e mergulhamos nas profundezas do padrão IEEE 754.
Agora você entende que quando o computador exibe \(0.30000000000000004\), ele não está “errado” ou “quebrado”. Ele está apenas sendo finito.
O Ponto Flutuante é, acima de tudo, uma obra-prima de Engenharia de Compromissos (Trade-offs):
- O Compromisso do Alcance: Para conseguirmos representar a massa do Sol e a massa de um próton na mesma variável de 32 bits, tivemos que aceitar que a vírgula flutue.
- O Compromisso da Precisão: Para ganhar esse alcance astronômico, tivemos que aceitar que não podemos guardar todos os números reais. Existem buracos na reta numérica do computador, e números como \(0.1\) caem nesses buracos, precisando ser arredondados.
- O Compromisso da Velocidade: Decisões como o Viés do Expoente e o Bit Escondido foram tomadas para economizar transistores e ciclos de processamento, permitindo que placas de vídeo calculem bilhões de polígonos por segundo.
O que levar para a vida profissional? #
Agora que você conhece a “Matrix” dos números reais, aqui estão as regras de ouro para sobreviver:
- Saiba onde usar o Float: Ele é perfeito para gráficos 3D, cálculos científicos, física de jogos e estatística, onde a velocidade e o alcance superam a necessidade de precisão absoluta na 15ª casa decimal.
- Saiba onde NÃO usar o float: Cuidado ao usar
floatoudoublepara sistemas bancários ou contábeis. Dinheiro exige precisão decimal exata. Use bibliotecasDecimal. - Cuidado com a Igualdade: Nunca compare floats com
==. Use sempre uma margem de erro (tolerância). - Cuidado com a Subtração: Lembre-se do “Cancelamento Catastrófico” ao subtrair números muito próximos.
O computador é uma máquina incrivelmente precisa, mas ele só é tão bom quanto o programador que entende as suas limitações físicas.
Por esse artigo é isso, pessoal. Até a próxima!