Je naozaj typ double presnejší ako float?
Pri jednej aplikácii, ktorá násobila "reálne" čísla som narazil na zaujímavú vec. Možno ste sa už s tým stretli. V gride som mal 3 stĺpce. Dve čísla a ich súčín. Keď som zadal 3 a 3,3, tak v tretom stĺpci sa objavilo číslo 9.8999999999999986.
Prvá vec, čo sa z toho dá zistiť, tak DataGridView (a ani Watch a Immediate window) nepoužívajú metódu ToString na zobrazenie hodnoty bunky. Skúste si do Immediate window napísať:
? 3.0 * 3.3
? (3.0 * 3.3).ToString()
Takže stačilo do stĺpca DataGridView zadať formát "G", a tým vynútiť volanie ToString(), a potom sa zobrazil správny súčin 9,9.
Ale samozrejme, že ma zaujímalo, čím sú tieto dve čísla take výnimočné. A aby to bolo ešte zamotanejšie, tak môžete vyskúšať tento kód.
float fa = float.Parse("3,0");
float fb = float.Parse("3,3");
float fc = fa * fb;
float fr = float.Parse("9,9");
Console.WriteLine("Float: {0}", fc == fr);
double da = double.Parse("3,0");
double db = double.Parse("3,3");
double dc = da * db;
double dr = double.Parse("9,9");
Console.WriteLine("Double: {0}", dc == dr);
Výsledok je nasledovný. Ináč rovnaký výsledok bude, aj keď namiesto Parse funkcie použijem konštanty.
Float: True
Double: False
Dosť bolo ukážok kódu, ktorý funguje ináč, ako by sme očakávali. Tak som išiel na Wikipediu a pozrel si článok o norme IEEE 754-1985, podľa ktorej pracuje C# (a aj iné jazyky) s číslami s pohyblivou čiarkou. Problém je v čísle 3,3. Toto číslo je totiž v binárnej sústave periodické: 11,010011. Takže žiaľ číslo 3,3 nie je možné reprezentovať podľa normy IEEE 754 presne. Rovnako ani číslo 9,9. Ale poďme sa pozrieť na to, prečo predchádzajúci kód správne porovná výsledky pre float, ale nie pre double.
Čísla s pohyblivou desatinnou čiarkou sú uložené v nasledovnom formáte:
- s = 1 bit pre znamienko. 0 znamená kladné číslo, 1 znamená záporné.
- e = x bitov pre exponent. Exponent je uložený bez znamienka, avšak pri čítaní exponentu treba odpočítať číslo 2x-1-1. Čiže ak je exponent uložený v 8 bitoch, tak pre e = 0 je v pamäti uložené číslo 0+28-1-1=127.
- f = y bitov pre desatinnú časť. Čísla sú podľa normy normalizované tak, že f je vždy v tvare 1,... (teda jedna cela niečo). Podľa toho sa vždy upraví exponent. Keďže f je vždy v takom tvare, tak sa prva jednotka neukladá, ale uložia sa iba bity za desatinnou čiarkou.
Takže výsledné číslo vypočítame podľa vzorca
s . f . 2e-b
kde b je základ, o ktorý je exponent posunutý.
Float
Typ float je uložený v 32 bitoch. 1 bit pre znamienko, 8 bitov pre exponent a 23 bitov pre desatinnú časť, pričom exponent je posunutý o 127. Hmm, teraz som si uvedomil, že desatinná časť v binárnej sústave znie dosť divne, ale mýslím tým číslice za desatinnou čiarkou (alebo binárnou čiarkou?).
Tak ešte jeden kúsok kódu. Tento program serializuje float číslo, a potom vypíše bit po bite jeho serializovanú podobu.
float n = 1.0F;
byte[] buffer = null;
using (var ms = new MemoryStream())
{
var bw = new BinaryWriter(ms);
bw.Write(n);
buffer = ms.ToArray();
}
for (int i = buffer.Length - 1; i >= 0; i--)
{
for (int shift = 7; shift >= 0; shift--)
{
int cb = buffer[ i ];
cb >>= shift;
cb &= 1;
Console.Write(cb);
}
Console.Write(' ');
}
Asi ste si všimli, že byty čítam v opačnom poradí. Je to preto, že sú v opačnom poradí zapísané. Číže najmenej významný byte je uložený ako prvý a najvýznamnejší byte je uložený ako posledný.
A reprezentácia čísla 1 je nasledovná:
s e f
0 01111111 00000000000000000000000
ďalej čísla:
| 3 = | 0 10000000 10000000000000000000000 |
| 3,3 = | 0 10000000 10100110011001100110011 |
| 3 . 3,3 = | 0 10000010 00111100110011001100110(01) |
| | Bity v zátvorke sú tie, ktoré sa vypočítali, ale zahodili, pretože sa nezmestia do typu float. |
| 9,9 = | 0 10000010 00111100110011001100110 |
| | Toto je serialozovan číslo, nie vypočítané. |
Z predchádzajúcej tabuľky je vidno, že číslo 3 . 3,3 a 9,9 majú rovnakú binárnu reprezentáciu pre typ float. Preto aj porovnanie v predchádzajúcom programe vyšlo pozitívne.
Double
Typ double je uložený v 64 bitoch. 1 bit pre znamienko, 11 bitov pre exponent a 52 bitov pre desatinnú časť, pričom exponent je posunutý 1023. Takže si zase spravme podobnú tabuľku.
| 3 = | 0 10000000000 1000000000000000000000000000000000000000000000000000 |
| 3,3 = | 0 10000000000 1010011001100110011001100110011001100110011001100110 |
| 3 . 3,3 = | 0 10000000010 0011110011001100110011001100110011001100110011001100(10) |
| 9,9 = | 0 10000000010 0011110011001100110011001100110011001100110011001101 |
A tu je vidno, že pre typ double je rozdiel v najnižšom bite medzi číslami 3 . 3,3 a 9,9. Preto sa pri porovnaní tieto čísla nerovnajú. Rozdiel je pravdepodobne spôsobený rozdielným prístupom k zaokrúhlovaniu. Pri násobení (a asi aj pri iných operáciách) sa nadbytočné bity jednoducho zahodia, teda sa vždy zaokrúhluje nadol. Avšak metóda Parse pravdepodobne vypočíta číslo s väčšou presnosťou, a potom ho správne zaokrúhli.
Pri type float sa tento rozdiel neprejavil, pretože orezávaná časť začínala bitom 0, takže sa zaokrúhlovalo nadol. Avšak pri type double orezávaná časť začínala bitom 1, takže tu sa prejavil rozdiel medzi zaokrúhlovaním a orezávaním.
Záver
Nakoniec som sa dozvedel, že v praxy sa neporovnávajú čísla s desatinnou čiarkou exaktne, ale s nejakou odchylkou. Odchylka pravdepodobne závisí od exponentu.
Avšak ešte som nezistil ako funguje metóda ToString, keď vypíše číslo 3,3 a nie niečo ako 3,299999999999999.