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:

  1. s = 1 bit pre znamienko. 0 znamená kladné číslo, 1 znamená záporné.
  2. 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.
  3. 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.

Bookmark and Share

Komentáre

# Miloslav Ponkrác said:

Tak tohle je základní teorie programátora, kterou před deseti lety ovládal programátor ještě dřív, než napsal první Hello world program.

Za prvé - je rozdíl mezi "přesností čísla" a "způsobem výpisu čísla". Například já napsal svoje vlastní podprogramy pro výpis reálných čísel a pomocí inteligentních algoritmů vypíšete číslo velmi rozumně a 3.3 vypíšete 3.3, a ne jako 3.299999998 například.

Pokud při zkoumání reálných čísel věříte a řídíte se jen podle vypisovacích rutin, jste naiva. Výpis reálného čísla je jen pokus o přiblížení se realitě, ale většinou je "uvnitř reálného čísla" něco maličko jiného - vypsat reálné číslo přesně jde jen binárně.

Stejně tak na skutečně rozumné zkoumání reálných čísel je třeba opustit MS vývojové nástroje - protože MS právě má nedotáhnutou podporu reálných čísel.

Monday, April 14, 2008 3:12 PM
# duffyx said:

K tomuto dotazu by som chcel dodať, že spracovanie môže naozaj závisieť od toho ako je program kompilovaný.

Pri vývoji jedneho algoritmu na komprimáciu obrázkov sme dostávali rozdielne výsledky v radovo jednotkách pri rôznych kompiláciách, hlavne pri Debug / Release. Niekedy si to človek ani neuvedomí predtym ako začne písať program, že napr. v Debug mode je spôsob spracovania čísla (napr. float) iný ako v Release. Nevýhoda je hlavne v tom, že práve v Debug sa programy ladia.

Ako to bolo spomenuté, kedysi sme museli uvažovať ešte predtým ako sa začal písať program. Teraz programátori píšu a k podstate sa neodstanú, vo väčšine prípadov na to nie je ani čas.

Sunday, May 11, 2008 10:53 AM
# Marián Košťál said:

Velmi pekny clanok. Kod na vypis cisla bit po bite je na moj vkus prilis zlozity (nechce sa mi nad tym rozmyslat) :) tak som hladal sposob ako to zjednodusit a dopracoval som sa k tomuto:

float f = 1.0F;

BitArray bits = new BitArray(BitConverter.GetBytes(f));

for (int i = bits.Length - 1; i >= 0; i--)

{

   Console.Write(bitsIdea ? 1 : 0);

   if (i % 8 == 0) Console.Write(' ');

}

Wednesday, July 02, 2008 4:40 PM
# duracellko said:

pekne.. triedu BitConverter som nepoznal

Monday, July 07, 2008 3:27 PM
# Skippov blog said:

Zacnime vtipom: "Rada nad zlato. Chcete zvacsit svoj kapital? Pozerajte si vyplatu pod lupou."

Thursday, December 11, 2008 11:23 AM
Prihlásiť | Registrovať | Pomoc