Por que números de ponto flutuante (Float e Double) são imprecisos?

Objetivo

Esses dias fui questionado por um colega de trabalho sobre usar números de ponto flutuante (float ou double) ou o tipo decimal para representar números.

Confesso que nunca tinha refletido a fundo nessa questão e aproveitei a oportunidade para pesquisar melhor o tema. Já tive muitos problemas com imprecisões em floats em casas decimais menos significativas, mas nunca fui pesquisar a razão.

A idéia desse post é compartilhar o aprendizado.

Representação interna

Como bem sabemos, todo tipo de dados tem uma representação interna, binária para o seu conteúdo. Essa representação é criada de uma forma a acomodar a maior quantidade de informações possíveis, no menor espaço. A mesma regra segue para os números de ponto flutuante.

Neste caso específico, optou-se por sacrificar um pouco a precisão em troca de espaço e velocidade de computação. Logo, a representação interna de um float (32 bits) é a seguinte (da esquerda para a direita):

  • 1 bit: sinal. 0 para positivo, 1 para negativo
  • 8 bits: expoente, representado em offset binary
  • 23 bits: mantissa

Esta notação segue um padrão IEEE 754.

Expoente

A notação do expoente, apesar de não ser lá tão simples, não é muito controversa. Ela segue a mesma regra de números inteiros (potências de 2), da direita pra esquerda. Ou seja, o primeiro bit da direita, representa 2^0, o segundo 2^1 até o oitavo bit 2^7.

A única diferença é que o “0” na verdade começa num offset de -127. Todos os bits zerados no expoente significa -127. Ao ligar o primeiro da direita, -126, o primeiro e o segundo, -124, de forma que quando ligamos todos os bits exceto o primeiro da esquerda (01111111), temos 2^0 = 1.

Mantissa

A mantissa é uma notação muito interessante. Quando todos os 23 bits estão desligados, não adicionamos nada ao número. Quando ligamos o primeiro bit mais significativo, ele adiciona metade do valor do expoente. O segundo bit, metade da metade do valor do expoente e assim sucessivamente.

Ex.: Se o expoente for 100000000 (decimal 2), o primeiro bit mais significativo do mantissa vale 1, o segundo vale 0,5, o terceiro 0,25 e assim sucessivamente.

Se o expoente for 011111111 (decimal 1), o primeiro bit mais significativo do mantissa vale 0,5, o segundo 0,25 e assim sucessivamente.

Se quisermos por exemplo o número 2.75, teremos expoente 100000000, mantissa (sem completar os digitos menos significativos) 011.

Outra curiosidade, se usarmos o expoente 100000000 e somente ligar o bit menos significativo do mantissa, seu valor é 0,0000002.

Tá bom, e as imprecisões?

Essa é a parte divertida. Por causa dessa característica da notação, alguns números não podem ser representados! Um bom exemplo é o número 0.1. Na verdade ele é composto por:

Sinal: 0
Expoente: 01111011 = 2^-4 = 0,0625
Mantissa: 10011001100110011001101 = 0,0375000014901161 (ver tabela abaixo)

Bit

Status

Valor
1

1

0,031250000000000000000000
2

0

0,015625000000000000000000
3

0

0,007812500000000000000000
4

1

0,003906250000000000000000
5

1

0,001953125000000000000000
6

0

0,000976562500000000000000
7

0

0,000488281250000000000000
8

1

0,000244140625000000000000
9

1

0,000122070312500000000000
10

0

0,000061035156250000000000
11

0

0,000030517578125000000000
12

1

0,000015258789062500000000
13

1

0,000007629394531250000000
14

0

0,000003814697265625000000
15

0

0,000001907348632812500000
16

1

0,000000953674316406250000
17

1

0,000000476837158203125000
18

0

0,000000238418579101562000
19

0

0,000000119209289550781000
20

1

0,000000059604644775390600
21

1

0,000000029802322387695300
22

0

0,000000014901161193847700
23

1

0,000000007450580596923830

Se fizermos a soma da tabela acima, chegamos no resultado 0,0625 + 0,0375000014901161 = 0,100000001490116.

A imprecisão 0,000000001490116 ocorre, porque na representação do número em bits não dá pra chegar exatamente nos 0.1.

Qual a implicação disso

Geralmente quando fazemos uma conta diretamente com float, não sentimos essa diferença, pois está numa casa decimal pouco significativa.

O grande porém está quando acumulamos os valores. Veja o snippet:

		static void Main(string[] args)
		{
			decimal d = 0m;

			for(int i = 0; i < 100000; i++)
				d += 0.1m;

			float f = 0F;
			for (int i = 0; i < 100000; i++)
				f += 0.1F;

			Console.WriteLine("Decimal: " + d.ToString()); //Decimal: 10000,0
			Console.WriteLine("Float: " + f.ToString()); //Float: 9998,557
		}

Era isso!

Referências

Utilizei como referência a Wikipedia e esta calculadora genial de números de ponto flutuante.

Também consultei o padrão IEEE 754 e a documentação da Microsoft.

Anúncios

5 comentários em “Por que números de ponto flutuante (Float e Double) são imprecisos?

    1. Oi Sidney,

      Ainda não pesquisei os detalhes do decimal. A princípio achei que fosse um BCD (Binary coded decimal), igual campo COMP-3 de Mainframe. Este tipo armazena como se fosse uma string, mas armazena um algarismo numérico a cada 4 bits, ou seja, num byte vc armazena 2 algarismos, invés de 1. Mas não é assim.

      Acredito que seja similar ao BCD, só que com outra notação. Quando vc manda fazer o cálculo, ele provavelmente converte pra float e depois devolve novamente na representação dele. Mais lento, mas não sofre do mesmo arredondamento.

      Que bom que o post foi útil. Não pensei q tinha tanta gente interessada nisso!

      Abraço,

      Eric

  1. Wiodaca role przejal w polowie lat 90. HTML Help, czy szablonowy struktura pomocy w aplikacjach Windows 95 tudziez jego nastepcow. Bazujac na dokumentach HTML, nadawal sie zgola az do tworzenia ksiazek elektronicznych, co zawdzieczal sila utworzenia dobrego systemu nawigacyjnego, skorowidza badz systemu wyszukiwawczego. tez tudziez tutaj zbytnio ostateczna postac odpowiada kompilator Microsoftu, oddanie zreszta niedobry, gdyz nie potrafiacy skompilowac rozmaitych formatow multimediow. nie predzej stosowanie pewnych sztuczek pozwalalo wkomponowac sposrod archiwami dodatkowe formaty, m.in. PDF.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s