Zwevendekommagetal
Een zwevendekommagetal of drijvendekommagetal, verouderd ook vlottendekommagetal (Engels: floating-point number) is een gegevenstype dat een vaste geheugenruimte beslaat en een grote variëteit aan getallen kan bevatten, van zeer kleine tot zeer grote. Hoewel zwevendekommagetallen strikt genomen rationale getallen zijn, worden ze meestal gebruikt als benadering voor reële getallen. De relatieve nauwkeurigheid waarin getallen door zwevendekommagetallen worden gerepresenteerd, is min of meer gelijk over het gehele bereik. Het is de digitale versie van de wetenschappelijke notatie.
Computers
bewerkenHet gegevenstype dat zwevendekommagetallen voorstelt, wordt binair opgeslagen in een vast aantal bits. De notatie als zwevendekommagetal maakt het mogelijk getallen van verschillende orden van grootte op te slaan. Niet ieder getal is echter als zodanig representeerbaar, zodat er in bijna alle gevallen een afrondfout is.
Gegevenstype
bewerkenDe zogenaamde type specifier kan, afhankelijk van programmeertaal, bijvoorbeeld real, float, single, extended, double, enzovoorts worden genoemd. Het gegevenstype wordt vaak verward met het wiskundige begrip rationaal getal, wat een bron van bugs bij zwevendekommagetallen is. Om berekeningen met zwevendekommagetallen te versnellen is vaak een aparte floating-point unit aanwezig. Ook verschillen tussen FPU's kunnen voor bugs zorgen.
Een zwevendekommagetal is een digitale representatie van een rationaal getal. In iedere implementatie zijn er maar eindig veel zwevendekommagetallen. Ze worden gebruikt in computers om reële getallen, zij het benaderend, voor te stellen. Een zwevendekommagetal wordt voorgesteld als een geheel getal of vastekommagetal, de mantisse, vermenigvuldigd met een grondtal, meestal 2 of 10, verheven tot een bepaalde macht, de exponent. Deze manier van voorstellen is het digitale analogon van de wetenschappelijke notatie.
Voor het zwevendekommagetal , bepaald door de mantisse en de exponent bij het grondtal, de radix , geldt:
De grootheden en hebben elk hun eigen bereik, afhankelijk van de gebruikte implementatie, de radix staat in de regel vast.
Een zwevendekommaberekening is een rekenkundige bewerking met zwevendekommagetallen. In het gunstigste geval is het resultaat van een bewerking de werkelijke uitkomst, afgerond op het dichtstbijzijnde representeerbare getal. Het verschil is de afrondfout. Bij een keten van berekeningen kunnen dergelijke fouten cumuleren, waardoor niet alle cijfers in het resultaat significant zijn.
Eigenschappen van zwevendekommaberekeningen
bewerkenBij bewerkingen met zwevendekommagetallen kan, vanwege de eindige nauwkeurigheid, de volgorde waarin de bewerkingen gebeuren verschil maken voor de uitkomst. In het algemeen geldt voor zwevendekommagetallen en :
Hetzelfde geldt voor de distributieve eigenschap. In het algemeen geldt:
Soms is het verschil groot en is een bepaalde volgorde of meer algemeen een bepaald algoritme duidelijk beter dan een andere, ook al zijn ze wiskundig gelijkwaardig, in de zin dat bij exact rekenen de uitkomst hetzelfde zou zijn. De numerieke wiskunde houdt zich hier mee bezig.
Zwevendekommagetallen als foutbron
bewerkenOm deze reden is het niet aan te raden twee zwevendekommagetallen direct met elkaar te vergelijken, zoals in het volgende voorbeeld, dat in C is geschreven:
#include <math.h>
#include <stdio.h>
int main(void)
{
float foo = 2 * M_PI / 3;
float bar = 8 * M_PI / 12;
/* FOUT */
if (foo == bar)
{
printf("ja, hij doet het\n");
}
return 0;
}
Nog erger is het in een lus:
#include <math.h>
#include <stdio.h>
int main(void)
{
float foo;
float bar = 8 * M_PI / 12;
/* FOUT */
for (foo = 0.0; foo != bar; foo += M_PI / 10)
{
printf("En nog een keer...\n");
}
return 0;
}
Omdat de niet-significante bits wel verschillen, maar toch worden meegenomen in de vergelijking, kunnen foo en bar van elkaar verschillen. Dit levert bijna gegarandeerd niet het resultaat waar men op rekent. Erger nog, het kan op verschillende computers verschillend gedrag vertonen.
Een correcte manier om twee zwevendekommagetallen met elkaar te vergelijken is:[1]
#include <math.h>
#include <stdio.h>
#define EPSILON (1E-10)
int main(void)
{
float foo = 2 * M_PI / 3;
float bar = 8 * M_PI / 12;
if (abs(foo - bar) < EPSILON)
{
printf("natuurlijk doet hij het...\n");
}
return 0;
}
Als de absolute waarde van het verschil tussen foo en bar kleiner is dan een schatting van de maximale fout, zijn ze aan elkaar gelijk. Deze fout epsilon verschilt per toepassing, aangezien de aard van de toepassing en de berekeningen die deze met zich meebrengt, de cumulatieve fout bepaalt.
Een andere bron voor vreemde resultaten is het aftrekken van twee waarden die bijna aan elkaar gelijk zijn. Aangezien de meer significante cijfers aan elkaar gelijk zijn, blijven in het resultaat een veel geringer aantal of zelfs helemaal geen significante cijfers over. Men spreekt in dit geval van een 'gedegenereerd getal'.
#include <stdio.h>
int main(void)
{
float foo = 1.000001E-12;
float bar = 1.000002E-12;
printf("%g\n", (bar - foo));
return 0;
}
Levert onverwachte resultaten:
$ ./try_float 9.75782e-19
Dus niet 10−18, zoals eigenlijk zou moeten. De oorzaak is dat 1.000001E-12 en 1.000002E-12 te klein zijn, daarom worden afgerond en dat vervolgens deze afrondfout in het verschil doorwerkt. De resulterende fout is relatief groot in gevallen waarbij de absolute waarde van het resultaat veel kleiner is dan die van de oorspronkelijke getallen, zoals hier.
Interne codering
bewerkenIn het algemeen
bewerkenEen zwevendekommagetal kan in een systeem met grondtal worden gerepresenteerd door twee getallen en , namelijk als . In elk dergelijk systeem wordt een basis (grondtal of radix) gekozen en een nauwkeurigheid (het aantal cijfers die moeten worden opgeslagen). (de significant of mantisse) is een -cijferig getal van de vorm (waarin elk cijfer een geheel getal is uit het interval van 0 tot en met ). Als het eerste cijfer van ongelijk is aan 0, wordt het getal genormaliseerd genoemd. Veel systemen gebruiken een aparte tekenbit ( , die −1 of +1 representeert) en vereisen dat niet-negatief is. Daarbij is de exponent.
Deze methode maakt het mogelijk een groot bereik aan waarden te representeren binnen een gegeven veldgrootte, wat niet mogelijk is in een vastekommanotatie.
Voorbeeld: een zwevendekommagetal met vier decimale cijfers ( en een exponentbereik van ±4 kan worden gebruikt om 43210, 4,321, of 0,0004321 te representeren, maar heeft niet genoeg nauwkeurigheid om 432,123 en 43212,3 te representeren (wat afgerond zou worden tot 432,1 en 43210). Vanzelfsprekend is in de praktijk het aantal cijfers groter dan vier.
Voorts kennen zwevendekommarepresentaties vaak de bijzondere waarden +∞, −∞ (plus en min oneindig) en NaN (Not a Number, geen getal). Oneindigheden worden gebruikt als de resultaten te groot zijn om te worden gerepresenteerd en NaN's geven een ongeoorloofde bewerking aan, of een uitkomst die niet is te representeren als een reëel getal.
IEEE 754
bewerkenHet IEEE heeft in 1985 de standaard gedefinieerd (IEEE 754) voor het representeren van binaire zwevendekommagetallen. Er is een enkeleprecisievariant (single) van 32 bits (1 tekenbit, 8 exponentbits en 23 mantissebits) en een dubbeleprecisievariant (double) van 64 bits (1 + 11 + 52). Een reëel getal wordt gerepresenteerd als . We leggen hier uit hoe de enkele-precisievariant in detail werkt. De strekking van de dubbele precisie is hetzelfde.
Teken, exponent en mantisse
bewerkenDe tekenbit is 1 voor een negatief getal en 0 voor een positief getal.
De exponent van 8 bits is gecodeerd met een offset van 127. Normaal representeren de 8 bits de getallen 0 (binair 00000000) tot en met 255 (11111111). Daar moet nu 127 van afgetrokken worden. Dat wil zeggen dat nu de getallen –127 tot en met 128 weergegeven kunnen worden. Dus het getal 127 (01111111) betekent nu 0.
In een genormaliseerd binair getal is de eerste bit altijd 1, dus deze hoeft niet daadwerkelijk opgeslagen te worden. Dit cijfer wordt ook de 'hidden bit' genoemd.
Het complete getal
bewerkenWe hebben nu een genormaliseerd getal in de vorm:
- sxxxxxxxxmmmmmmmmmmmmmmmmmmmmmmm
waarbij:
- teken = dus als , dan teken = −1; als dan teken = 1
- exponent = xxxxxxxxxx
- mantisse = 1,mmmmmmmmmmmmmmmmmmmmmmm2; de 1 voor de komma is de verborgen bit.
De lager geschreven getallen 2 en 10 geven aan in welk talstelsel de waarden zijn genoteerd.
Het reële getal heeft de waarde:
- teken × mantisse × 2exponent.
Speciale waarden
bewerkenNu is het op deze manier niet mogelijk om het getal 0 te coderen. Om dit en nog wat andere speciale waarden mogelijk te maken zijn een aantal bitpatronen gedefinieerd.
Genormaliseerde getallen | 0 < exponent < 25510; | mantisse > 0; | zoals hierboven beschreven |
Gedenormaliseerde getallen | exponent = 0; | mantisse > 0; | waarden die te klein zijn voor normalisatie |
De waarde 0 | exponent = 0; | mantisse = 0; | −0, +0, en waarden die te klein zijn voor representatie |
Oneindig | exponent = 25510; | mantisse = 0; | waarden die te groot zijn voor representatie |
Not a number (NaN) | exponent = 25510; | mantisse > 0; | vele NaN's zijn mogelijk |
Deze standaard wordt in bijna alle computers gebruikt. De 'NaN' wordt gebruikt om het resultaat van een onmogelijke berekening weer te geven, zoals de vierkantswortel van een negatief getal. De waarde oneindig wordt gebruikt om resultaten weer te geven van berekeningen waarvan het resultaat te groot is om weer te geven, zoals een deling door een heel klein getal of nul.
Alle speciale waarden hebben een positieve en een negatieve variant, aangezien de tekenbit op zichzelf staat.
Bronnen
bewerken- D Goldberg. What Every Computer Scientist Should Know About Floating-Point Arithmetic, 1 maart 1991. voor ACM Computing Surveys, 23, 1, blz 5-48
- W Kahan en J Darcy. How Java’s Floating-Point Hurts Everyone Everywhere, 30 juli 2004.