Ponto Flutuante e a origem dos “Números Esquisitos”();

Esses dias “lá na firma” alguém me perguntou se os números fracionários eram gravados no banco de dados com ponto ou com vírgula, na separação de casas decimais. Eu respondi que não era nenhum dos dois. Umas duas ou três semanas depois, apareceu no registro de uma tabela, um valor bem estranho onde deveria estar armazenado um peso em Kg: 13.170000000000001. Acredito que o dado que deveria estar armazenado era 13,170 Kg (treze quilos e cento e setenta gramas), mas por que aquele dígito na décima quinta casa decimal? Você sabe como essas duas coisas estão relacionadas?

Ponto flutuante

Ponto flutuante é a maneira mais comum que os computadores usam para armazenar e processar números fracionários. É uma forma de se trabalhar com números reais com uma certa aproximação, e deve seguir padrões de implementação tanto no hardware quanto no software.

Um número em notação de ponto flutuante é representado da seguinte forma:

mantissa x base expoente

Exemplos:

  • Número 314 -> 3.14 x 102 (em decimal, também pode ser representado como 3.14E2)
  • Número 0,0025 -> 2.5 x 10-3 (em decimal, também pode ser representado como 2.5E-3)
O padrão IEEE-754

O padrão IEEE-754 foi criado em 1985 para tratar diversos problemas encontrados em várias implementações de ponto flutuante, que causavam problemas de confiabilidade e portabilidade. Esse padrão estabelece o formato de precisão simples e precisão dupla (entre outros, mas esses dois são os mais usados), para armazenamento de valores em ponto flutuante. Segundo o padrão, os números de precisão simples devem ser armazenados em uma expressão de 32 bits, enquanto os de precisão dupla são armazenados em 64 bits, conforme o esquema abaixo:

Como os valores de ponto flutuante são armazenados em 32 ou 64 bits.

Em resumo, o padrão IEEE-754 estabelece que para armazenar um valor em ponto flutuante deve-se usar o seguinte método:

  • Precisão simples: 1 bit de sinal, 8 bits para armazenar o valor do expoente e 23 bits para armazenar o valor da mantissa.
  • Precisão dupla: 1 bit de sinal, 11 bits para armazenar o valor do expoente e 52 bits para armazenar o valor da mantissa.

Isso responde a primeira pergunta: No banco de dados, os números não são armazenados nem com ponto e nem com vírgula. Eles são armazenados normalmente num formato de 32 ou 64 bits dependendo da precisão. Por exemplo, o tipo de campo REAL do PostgreSQL é compatível com o padrão IEEE-754 de precisão simples (32 bits), enquanto que o tipo DOUBLE é de precisão dupla (64 bits).

E o número esquisito?

Ah, essa é uma parte bem curiosa. Abra o console Javascript do seu navegador e digite:

				
					x = 0.1 + 0.2
				
			

Essa é a parte em que muita gente pensa “Ué… vai dar 0.3, não?”. Vou colocar abaixo o “print” do resultado:

Experimento no console Javascript do navegador.

Não, o navegador não está “bugado” e nem é problema do Javascript. Pode ter passado batido, mas lá no primeiro parágrafo que fala sobre ponto flutuante, eu digo que “é uma forma de se trabalhar com números reais com uma certa aproximação”. Isso acontece porque não é possível representar uma quantidade infinita de valores usando uma quantidade finita de recursos. Veja bem: não estou dizendo que não é possível representar o valor infinito. Estou dizendo que entre 0 e 1, matematicamente há uma quantidade infinita de valores, e não é possível representar todos esses valores possíveis usando apenas 32 ou 64 bits, então erros de aproximação estão sujeitos a acontecer em operações matemáticas com ponto flutuante. Eu falo sobre isso no capítulo 2 do livro que eu estou escrevendo.

Veja o mesmo exemplo feito na linguagem Rust:

Teste de operação de ponto flutuante feito em linguagem Rust.
Valores especiais do padrão IEEE-754

A especificação de 1985 do padrão IEEE-754 permite alguns valores especiais:

  • O valor NaN (que significa “Not a Number”) é usado para representar números indefinidos ou fora do conjunto dos números reais, como 0 dividido por 0, raiz quadrada de um número negativo, entre outros.
  • Existe uma forma de representar o valor infinito, resultado de 8 dividido por 0, por exemplo.
  • Devido à forma como o sinal é armazenado em relação aos demais campos, pode haver operações que resultam em -0 (menos zero), dependendo da linguagem e do compilador usados.

Esses valores são armazenados de acordo com as seguintes regras:

ValorBit de SinalExpoenteMantissa
NaN0 ou 1Todos os bits 1Pelo menos um bit 1
Infinito Positivo0Todos os bits 1Todos os bits 0
Infinito Negativo1Todos os bits 1Todos os bits 0

Veja esse novo exemplo em linguagem Rust:

Números especiais do padrão IEEE-754, demonstrados em linguagem Rust.
Indo mais além

Esse site aqui mostra de maneira bem detalhada, como os valores em ponto flutuante são armazenados bit a bit.