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.
Eric, excelente post.
Então a implementação de decimal é diferente? Como ela é implementada internamente para que não seja bugada como o Float?
Agora posso dizer com certeza absoluta que a leitura valeu mais que 0,01 centavos para mim. 🙂
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
Parabéns.
This is a attention-grabbing post by the way. I am going to go ahead and bookmark this post for my sis to check out later on tonight. Keep up the superior work.
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.