Na závěr této kapitoly učiníme několik důležitých poznámek k reprezentaci čísel v počítačích. Nejedná se o zcela podrobný výklad této problematiky. Je ale dobré si uvědomit, zvlášť pro studenty fakulty informatického zaměření, jaký je rozdíl mezi reálnými čísly a „strojovými čísly“ („float“). Zkušenost ukazuje, že i ve vyšších ročnících s těmito koncepty studenti zápasí.
Nejprve připomeňme známá fakta z BI-DML.
Číselné množiny $\N$, $\Z$, $\Q$ i $\R$ jsou nekonečné, tj. nemají konečný počet prvků.
Množiny $\N$, $\Z$ i $\Q$ jsou nekonečné a spočetné (mají stejnou mohutnost jako $\N$).
Množina reálných čísel $\R$ je dokonce nespočetná (Cantorův diagonální argument).
Protože paměť počítačů je omezená, je zřejmé, že v celé obecnosti nelze ani jednu z těchto množin v počítači reprezentovat. Jak uvidíme na konci této podkapitoly, tak nedostatečnost reprezentace v případě reálných čísel $\R$ je zásadnější než v případě $\N$, $\Z$ a $\Q$.
Celá čísla lze snadno reprezentovat v binární soustavě, tj. celé číslo $k \in \Z$ lze vyjádřit například ve tvaru
kde $n \in \N_0$ je přirozené číslo a $k_0,\ldots,k_n \in \{0,1\}$.
Typicky pro fixní $n$ v paměti ukládáme znaménko (jeden bit) a $k_0,\ldots,k_n$ ($n+1$ bitů), tedy v paměti zaberem $n+2$ bitů. Tímto způsobem pokryjeme jistou konečnou podmnožinu množiny $\Z$. I kdybychom $n$ neomezili (libovolná přesnost, viz např. GNU MP (GMP) knihovnu), pak pro většinu hodnot narazíme na konečnou paměť našeho stroje.
Například pro 64 bitový integer se znaménkem je největší, resp. nejmenší, reprezentovatelná hodnota
Což, přiznejme si, v porovnání s většinou celých čísel nejsou žádné velké hodnoty.
Algebraické operace mezi takto popsanými celými čísly probíhají exaktně a nedochází při nich k chybám, vyjma problému s přetečením a podtečením.
Pokud bychom měli pro náš číselný typ k dispozici pouze čtyři bity, pak by situace vypadala následovně. V paměti bychom měli uložen jeden bit $s \in \{0,1\}$ pro znaménko a tři bity $k_0, k_1, k_2 \in \{0,1\}$ pro binární cifry. Každému řetězci $sk_0k_1k_2 \in \{0,1\}^4$ je pak přiřazeno číslo
V tomto případě můžeme všechna tato čísla i pěkně vypsat, dekódovací tabulka je znázorněna v Tabulce 2.1. Máme tak konečnou množinu o $2^4 = 16$ členech popisující celá čísla od $-8$ do $7$, tedy množinu $\{-8,-7,\ldots,6,7\}$. To je samozřejmě velmi malinká podmnožina množiny $\Z$. Aritmetické operace sčítání a odčítání lze provádět exaktně, správně zde dostaneme například $2 \cdot (-3) = -6$ nebo $-2 + 7 = 5$. Velmi často ale narazíme na fatální přetečení/podtečení (overflow / underflow).
$sk_0k_1k_2$ | $f(sk_0k_1k_2)$ | $sk_0k_1k_2$ | $f(sk_0k_1k_2)$ |
---|---|---|---|
$0000$ | $0$ | $1000$ | $-1$ |
$0100$ | $1$ | $1100$ | $-2$ |
$0010$ | $2$ | $1010$ | $-3$ |
$0110$ | $3$ | $1110$ | $-4$ |
$0001$ | $4$ | $1001$ | $-5$ |
$0101$ | $5$ | $1101$ | $-6$ |
$0011$ | $6$ | $1011$ | $-7$ |
$0111$ | $7$ | $1111$ | $-8$ |
Racionální čísla můžeme v paměti uchovávat jakožto dvě celá čísla.
Algebraické operace mezi racionálními čísly vycházejí z algebraických operací mezi celými čísly a tedy i je lze vykonávat exaktně (bez chyby).
Stále ovšem hrozí problém nemožnosti reprezentovat příliš velký (v absolutní hodnotě) jmenovatel nebo čitatel.
Některé programovací jazyky (např. parametrický typ Rational{T}
v Julia, třídy std::ratio
v C++ a Fraction
v Pythonu, atp.) a různé CAS ( Mathematica, SageMath a příbuzní) umožňují pracovat s racionálními čísly tímto exaktním způsobem.
Znovu upozorňujeme na zásadní rozdílnost tohoto přístupu od přístupu založeném na číslech s plovoucí desetinnou tečkou (tzv. float), kterému se budeme věnovat v následující části textu.
Nejpoužívanějším standardem pro práci s čísly s tzv. pohyblivou desetinnou čárkou je standard IEEE-754 (IEEE, 2008). Tento standard je zcela jistě historicky nejrozšířenější a nejpoužívanější. Díky jeho podpoře na úrovni hardware, jsou výpočty s těmito čísly velmi rychlé. Za rychlost je ale nutné zaplatit nepřesností ve výpočtech, které mohou mít fatální důsledky pro výpočty. Implementace „matematického algoritmu“ tak nemusí být zcela jednoduchá, protože akumulace numerických chyb může některé z těchto postupů učinit prakticky nepoužitelné.
Nejedná se ovšem o jediný možný způsob práce s aproximací reálných čísel na počítači. Existují i další přístupy jako například Unum (universal numbers) nebo intervalová aritmetika. Do těchto oblastí v tomto textu zabíhat nebudeme. Zvídavé čtenářstvo se může dozvědět více z uvedených odkazů.
Reálná čísla můžeme popsat v binárním ciferném tvaru, jehož část za „desetinnou“ tečkou může ovšem být nekonečná. V závislosti na hodnotě mantisy $m \in \Z$, exponentu $e \in \Z$ (maximálně $d$ bitů), znaménka $s \in \{0,1\}$ a typového parametru $b \in \Z$ postupujeme takto:
Pro $0 < e < 2^d - 1$ a $m$ klademe $x = (-1)^s \cdot (1.m_2)_2 \cdot 2^{e - b}$ (tzv. normalizované číslo).
Pro $e = 0$ a $m \neq 0$ klademe $x = (-1)^s \cdot (0.m_2)_2 \cdot 2^{1-b}$ (tzv. subnormální číslo).
Pro $e = 0$, $m = 0$ a $s = 0$ klademe $x = +0$ (pro $s = 1$ pak $x = -0$).
Pro $e = 2^d - 1$, $m = 0$ a $s = 0$ klademe $x = +\mathtt{Inf}$.
Pro $e = 2^d - 1$, $m = 0$ a $s = 1$ klademe $x = -\mathtt{Inf}$.
Pro $e = 2^d - 1$ a $m \neq 0$ klademe $x = \mathtt{NaN}$ (Not a Number).
Omezení těchto parametrů definovaná ve standardu IEEE-754 jsou uvedena v Tabulce 2.2.
přesnost | mantisa $m$ | $d =$ počet bitů $e$ | parametr $b$ |
---|---|---|---|
poloviční (binary16) | $10$ bitů | $5$ | $15$ |
jednoduchá (binary32) | $23$ bitů | $8$ | $127$ |
dvojitá (binary64) | $52$ bitů | $11$ | $1\,023$ |
čtyřnásobná (binary128) | $112$ bitů | $15$ | $16\,383$ |
Tabulka 2.2: Parametry různých strojově číselných datových typů. Ve všech případech ještě máme jeden bit pro znaménko, $s \in \{0,1\}$.
Podobně jako v Příkladu 2.8 se pojďme podívat, jak by mohla vypadat takováto čísla ve velmi extrémně malém, ale konkrétním, příkladě $5$ bitů. Těchto $5$ bitů rozdělme následujícím způsobem:
$1$ bit pro znaménko $s \in \{0,1\}$,
$2$ bity pro signifikand $m_0, m_1 \in \{0,1\}$, tj. $m\in\{0,1,2,3\}$,
$2$ bity pro exponent $e_0, e_1 \in \{0, 1\}$, tj. $e\in\{0,1,2,3\}$.
Parametr $b$ zvolíme analogicky (viz Tabulku 2.2) jako $1$. Dále v souladu se značením výše máme $d = 2$.
Řetězce $sm_0m_1e_0e_1 \in \{0,1\}^5$, kterých je celkem $2^5 = 32$ různých, pak dekódujeme podle popisu výše následujícím způsobem.
Speciální hodnota +Inf
, resp. -Inf
, odpovídá situaci $e = 3$, $m=0$ a $s=1$, resp. $s=0$, tj. řetězcům
Kladná, resp. záporná, nula odpovídá situaci $e = m = 0$ a $s = 0$, resp. $s = 1$, tedy
Následujících šest řetězců splňujících $e = 3$ a $m \neq 0$ nepředstavuje žádná čísla, jde o NaN
,
Pro normalizovaná čísla máme hodnoty $e=1,2$, $m$ a $s$ libovolné, dostáváme tak celkem $2 \cdot 4 \cdot 2 = 16$ normalizovaných čísel
Pro subnormální čísla máme hodnoty $e=0$, $m \neq 0$ a $s$ libovolné, dostáváme tak celkem $1 \cdot 3 \cdot 2 = 6$ subnormálních čísel
Shrňme si stručně výsledky tohoto příkladu. Hypotetický pěti bitový datový typ popsaný v tomto příkladě svých $32$ hodnot interpretuje jako
$2$ speciální hodnoty +Inf
a -Inf
,
$2$ nuly $+0$ a $-0$,
$6$ nečíselných hodnot NaN
,
$16$ normalizovaných čísel, zkráceně zapsaných jako
$6$ subnormálních čísel, zkráceně zapsaných jako
Grafickou ilustraci uvádíme na Obrázku 2.8.
Z výše uvedeného je patrné, že z množiny $\R$ jsme schopni popsat vždy jen velmi omezenou a konečnou množinu tzv. strojových čísel. Navíc každé takové číslo je racionální.
Dále standard definuje, jak se zaokrouhluje při provádění algebraických operací a při ukládání čísel, která nejsou exaktně vyjádřitelná ve zvolené přesnosti. Většina algebraických operací s takovýmito čísly je tudíž nutně zatížena chybou. V důsledku toho operace mezi strojovými čísly ztrácí řadu očekávaných vlastností (jako asociativita nebo distributivita). Dále se tato chyba může postupně kumulovat, nebo i zásadně projevit i jen při jedné operaci.
Výhodou strojových čísel je samozřejmě rychlost operací, které probíhají na úrovni hardware. Cena za to je dána výše zmíněnými problémy. Při implementaci „matematického algoritmu“ je proto nutné se zabývat i vlivem zaokrouhlovacích chyb. Některé algoritmy nejsou pro implementaci ve strojových číslech z těchto důvodů vhodné, například Gaussova eliminace. Těmito problémy se (mimo jiné) zabývá numerická matematika.
Zaokrouhlování, vedle podtečení a přetečení, není jedinou patologií strojových čísel. Rozložení těchto čísel po číselné ose je silně nerovnoměrné. Daleko od nuly jsou mezery mezi čísly poměrně velké! Graficky lze tento efekt vyjádřit pomoci histogramu uvedeného na Obrázku 2.9.
Všimněte si, že každé strojové číslo je nutně tvaru podílu celého čísla a nějaké mocniny dvou. Ne každé takové číslo ovšem je strojové.
Uvažme standardní 64 bitová strojová čísla.
Jaké je největší a druhé největší číslo (Inf
teď neuvažujeme) přesně reprezentovatelné v tomto datovém typu?
Jaká je mezi nimi mezera?
Maximem je číslo $x = +(1.11\ldots11)_2\cdot 2^{2^{11} - 2 - 1023} = 2^{1024} - 2^{971}$. Druhým největším strojovým číslem je $y = +(1.11\ldots10)_2\cdot 2^{2^{11} - 2 - 1023} = 2^{1024} - 2^{972}$. Mezera mezi nimi je tedy velikosti $x - y = 2^{972} - 2^{971} = 2^{971}$.
Číslo $1$ je strojové číslo. V dvojité přesnosti určete nejmenší strojové číslo, které je větší než $1$. Tj. které „následuje“ hned za $1$.
$1 + \frac{1}{2^{52}} = \frac{4503599627370497}{4503599627370496}$.
Pojďme se podívat na konkrétní příklad. Uvažme v dnešní době standardní dvojitou přesnost (64 bitový float).
Číslo $\frac{1}{2}$ je rámci tohoto datového typu přesně reprezentovatelné jako normalizované strojové číslo, protože
Číslo $\frac{1}{3}$ už přesně reprezentovatelné není11, jeho binární rozvoj je nekonečný12:
Zde jsme zaokrouhlili směrem k nule. V počítači proto tímto způsobem nemůže být číslo $1/3$ uloženo přesně! Přesně je v něm uloženo výše zmíněné $q$.
Problémy s chybami při provádění algebraických operací můžeme nyní ilustrovat explicitně. Platí následující rovnosti:
kde $\approx$ opět znázorňuje, jaké číslo bychom dostali po zaokrouhlení do strojových čísel.
+Inf
a -Inf
V Definici 2.3 jsme zavedli (některé) algebraické operace mezi prvky množiny $\eR$.
Znovu připomínáme, že motivací pro tuto definici je Věta 6.1, které se budeme věnovat podrobněji později.
Standard IEEE-754 vedle popisu reprezentace strojových čísel a speciálních hodnot +Inf
, -Inf
, NaN
(Not a Number), +0
a -0
i určuje hodnoty algebraických operací, kde jeden z operandů je některá z těchto speciálních hodnot.
Operace mezi těmito speciálními hodnotami takřka přesně odpovídají naší Definici 2.3. Je zde ovšem několik drobných rozdílů.
Při sčítání a odčítání se strojová nekonečna +Inf
(zkráceně Inf
) a -Inf
chovají tak, jak bychom čekali.
Například platí: Inf + Inf = Inf
, -Inf - Inf = -Inf
, a + Inf = Inf
a a - Inf = -Inf
, zde a
je libovolné konečné strojové číslo (prvek $\Q$).
V případech, které jsme v Definici 2.3 vynechali, nyní dostaneme NaN
hodnotu.
Tedy v podstatě chybu, například: Inf - Inf = NaN
, -Inf + Inf = NaN
.
Při násobení jsme také ve shodě s Definicí 2.3.
Například platí Inf * Inf = Inf
, -Inf * Inf = -Inf
, a * (-Inf) = +Inf
pro záporné konečné strojové a
, atd.
Pokud násobíme nulou (jedno kterou, +0
zkráceně označujeme 0
) nekonečnou hodnotu, tak opět dostaneme chybu, resp. NaN
.
Například platí 0 * Inf = NaN
, 0 * (-Inf) = NaN
.
V souladu s Definicí 2.3 je i snaha o dělení dvou strojových nekonečen.
Tento případ není definován, dostaneme NaN
.
Například platí Inf / Inf = NaN
, -Inf / Inf = NaN
, atd.
Standard IEEE-754 zavádí kladnou nulu +0
(zkráceně zapisujeme 0
) a zápornou nulu -0
. „Nula bez přívlastku“ mezi strojovými čísly v pravém slova smyslu neexistuje.
Tyto dvě nuly jsou ale často považovány za totožné (např. porovnání +0 == -0
vrací true
).
Obě tyto nuly se chovají jako neutrální prvky vůči sčítání.
Z našeho úhlu pohledu toto není vhodné. V množině $\R$ i $\eR$ máme pouze jednu nulu $0$, která není ani „kladná“ ani „záporná“. Je to nula.
Zbývá shrnout zatím neprobrané situace týkající se dělení.
Zde se standard IEEE-754 od naší Definice 2.3 již zásadněji odlišuje, protože tu hraje roli znaménko strojové nuly.
Nejprve, podíl konečného strojového čísla a nekonečna je nula, jejíž znaménko se řídí znaménky čitatele a jmenovatele podílu.
Například platí: a / (-Inf) = +0
a a / Inf = -0
pro libovolné záporné konečné a
(včetně záporné nuly), atd.
Při dělení nulou pak mohou nastat dvě situace:
podíly dvou (libovolných) nul vrací NaN
, nejsou definovány.
Tj. +0 / +0 = NaN
, -0 / -0 = NaN
, +0 / -0 = NaN
a -0 / +0 = NaN
.
podíl, kde čitatel je nenulové strojové číslo (včetně nekonečných hodnot) a jmenovatel je jedna z nul, vyústí v nekonečnou hodnotu „se znaménkém daným znaménky čitatele a jmenovatele“.
Například platí: Inf / -0 = -Inf
, a / +0 = -Inf
pro libovolné záporné nenulové strojové a
, atd.
V tomto posledním odstavci se tedy Definice 2.3 a standard IEEE-754 zásadně odlišuje.
Není to překvapivé, standard a naše definice jsou v tomto směru nekompatibilní ($0$ není +0
ani -0
).
Kladná i záporná strojová nula se ovšem chová rozumně, zmíněnou Větu 6.1 by šlo upravit tak, aby vhodně motivovala i toto chování.
My se tímto směrem nevydáváme, v matematice nebývá zvykem zápornou a kladnou nulu používat.