Saltar para o conteúdo
Topos Uranos

Resumo

Os números com casas decimais de um programa não são os números reais da matemática: são um subconjunto finito de racionais cujo denominador é uma potência de dois, e todo valor que não pertence a esse subconjunto é substituído pelo mais próximo que pertence. Esta aula estuda como um número de ponto flutuante é guardado em 64 bits (sinal, expoente e mantissa), por que 0.10.1 não tem representação exata e que valor ocupa o seu lugar, quantos dígitos são seguros em float e em double, como se acumulam os erros de arredondamento, por que dois reais não se comparam com == e como se comparam com uma tolerância justificada, e o que são o infinito e o valor não numérico que o formato reserva para os resultados que não cabem ou não existem.

Objetivos de aprendizagem

  1. Explicar por que 0.10.1 não tem representação binária exata e que valor se guarda no seu lugar.
  2. Prever o efeito do arredondamento em somas repetidas e na soma de números de magnitudes muito diferentes.
  3. Comparar números reais com uma tolerância adequada em vez de ==, justificando a escolha da tolerância.
  4. Escolher entre float e double segundo a precisão requerida, e reconhecer o infinito e o valor não numérico.

Notação científica em base 2

A aula Os inteiros e sua representação mostrou que um inteiro de nn bits representa exatamente 2n2^n valores consecutivos. Ora, os números com casas decimais apresentam uma dificuldade diferente: entre dois reais quaisquer há infinitos outros, e nenhum número finito de bits pode distingui-los todos. A solução que todos os computadores atuais adotam é a mesma que a ciência usa para escrever magnitudes muito grandes ou muito pequenas: a notação científica. Em base 10, a distância média da Terra ao Sol escreve-se 1.496×10111.496 \times 10^{11} metros; o número tem um sinal, uns poucos dígitos significativos (a mantissa, 1.4961.496) e uma potência da base (o expoente, 1111), que diz onde vai a vírgula. Se isto é assim, com um número fixo de dígitos cobrem-se magnitudes de ordens muito diferentes, ao preço de guardar apenas os primeiros dígitos de cada uma.

O computador faz o mesmo em base 2. Todo número diferente de zero escreve-se de uma única maneira como

x=(−1)s×1.m1m2m3…m52 (2)×2ex = (-1)^{s} \times 1.m_1 m_2 m_3 \ldots m_{52} \, {}_{(2)} \times 2^{e}

onde ss é o sinal (0 para positivo, 1 para negativo), 1.m1m2…1.m_1 m_2 \ldots é a mantissa escrita em binário, cujo primeiro dígito é sempre 1, e ee é o expoente. Que o primeiro dígito seja sempre 1 tem uma consequência prática: não é preciso guardá-lo, e o bit que se poupa ganha-se para a precisão. O nome ponto flutuante descreve justamente esta forma: o ponto não ocupa um lugar fixo entre os dígitos, mas “flutua” conforme o expoente.

O tipo double de C++ usa, em todos os compiladores vigentes, o formato binário de precisão dupla normalizado internacionalmente, de 64 bits: 1 bit para o sinal, 11 bits para o expoente e 52 bits para a mantissa, sem o dígito inicial. O expoente é guardado deslocado: o campo de 11 bits contém e+1023e + 1023, de modo que guarda valores sem sinal de 1 a 2046 para expoentes de −1022-1022 a 10231023 (os valores 0 e 2047 do campo reservam-se para o zero, os números muito pequenos, o infinito e o valor não numérico, que se verão no final da aula). O tipo float usa o formato de precisão simples, de 32 bits: 1 de sinal, 8 de expoente e 23 de mantissa.

Os bits de um double podem ser vistos. O programa seguinte usa std::bit_cast, do cabeçalho <bit>, que reinterpreta os 64 bits do número como um inteiro sem sinal de 64 bits, sem mudar nenhum; aqui ele é usado sem mais explicações, como um instrumento para olhar a memória. A especificação {:064b} escreve esse inteiro em binário com 64 dígitos, como na aula anterior, e {:016X} em hexadecimal com 16.

Programa em C++
#include <bit>#include <cstdint>#include <print>​int main(){    const double tenth{0.1};    // Os 64 bits do número, lidos como um inteiro sem sinal.    const std::uint64_t bits{std::bit_cast<std::uint64_t>(tenth)};    std::println("{:064b}", bits);    std::println("{:016X}", bits);}
Saída
00111111101110011001100110011001100110011001100110011001100110103FB999999999999A
ExemploOs 64 bits de 0.1

A figura agrupa os 64 bits que o programa escreveu e os lê campo por campo.

Demonstração

  1. 1+11+52=641 + 11 + 52 = 64

    Os 64 bits, em hexadecimal 3FB999999999999A. Leem-se da esquerda para a direita: 1 bit de sinal, 11 de expoente e 52 de mantissa.

  2. s=0s = 0

    O sinal é 0: o número é positivo.

  3. e=1019−1023=−4e = 1019 - 1023 = -4

    O campo do expoente, 0111111101101111111011 em binário, vale 1019; subtrai-se dele o deslocamento, 1023, e o expoente é −4-4.

  4. m≈1.6000000000000000888m \approx 1.6000000000000000888

    A mantissa, com o 1 inicial que não se guarda, é 1.1001100110011…10101.1001100110011\ldots1010 em binário: o grupo 10011001 repete-se e o último grupo, 10101010, está arredondado para cima. Seu valor é aproximadamente 1.60000000000000008881.6000000000000000888.

  5. x=0.1000000000000000055511…≠0.1x = 0.1000000000000000055511\ldots \neq 0.1

    O número guardado é +1.6000000000000000888×2−4+1.6000000000000000888 \times 2^{-4}, isto é, 0.1000000000000000055511…0.1000000000000000055511\ldots: muito perto de 0.10.1, mas não 0.10.1.