Het artikel bespreekt de inherente onnauwkeurigheid van floating-point getallen bij het uitvoeren van decimale berekeningen, zoals het bekende voorbeeld waarbij 0.1 + 0.2 niet exact 0.3 oplevert. De auteur legt uit dat dit komt doordat decimale getallen niet altijd exact in een binaire floating-point representatie kunnen worden uitgedrukt.
Daarnaast wordt de technische opbouw van een IEEE double-precision float (64 bits) toegelicht, bestaande uit:
- 1 tekenbit
- 11 exponentbits
- 52 fractiebits
Het concept van de 'ulp' (unit in the last place) wordt geïntroduceerd om de kleinste mogelijke verandering in de waarde van een floating-point getal te beschrijven, wat helpt om te begrijpen waar de precisielimieten liggen.
Floating-point decimalen optellen voor het plezier en het gewin
Een beroemd voorbeeld is dat bij IEEE double-precision floats 0.1 + 0.2 niet 0.3 is, maar 0.30000000000000004.
Aan de andere kant gebruik ik constant een Python REPL om decimale getallen op bonnetjes op te tellen. Natuurlijk zijn de belangen bij mij lager; ik tel bedragen op in de range van $1 tot $100 en weet dat ik de microscopische fouten handmatig moet afronden voordat ik de som ergens kopieer. Maar eigenlijk gebeurt het heel vaak dat die fouten helemaal niet optreden.
Als we elk paar veelvouden van 0,01 optellen tot 1,00 en kijken of het geprinte resultaat te groot (blauw), te klein (rood) of correct is, ontstaat er een interessant patroon:
Afbeelding 1: Hoe floating-point invloed heeft op het optellen van twee veelvouden van 0,01, tot 1,00
Waar komt dit patroon vandaan?
Floats in het kort
Een floating-point getal met dubbele precisie ($x$) bestaat uit 1 tekenbit, 11 exponentbits en 52 fractiebits (in die volgorde), voor een totaal van 64 bits.
De tekenbit bepaalt het teken, $+$ of $-$. De exponentbits representeren een geheel getal $E$ tussen $-1022$ en $1023$, inclusief. De fractiebits representeren een niet-negatief geheel getal $F$ kleiner dan $2^{52}$, wat correspondeert met de significand $1 + F/2^{52} \in [1, 2)$; die altijd aanwezige $1$ wordt de "hidden bit" genoemd. De waarde van het floating-point getal is $x = \pm 2^E(1 + F/2^{52})$.
De effectieve waarde van de laatste fractiebit is dus $2^{E-52}$, een hoeveelheid die de ulp wordt genoemd, voor "unit in the last place", van $x$. De twee floating-point getallen die het dichtst bij $x$ liggen, verschillen er precies één ulp van, behalve in een randgeval aan één zijde wanneer $F = 0$ en $x$ precies een macht van twee is.
Voorbeeld: float(0.1) = 0.00011001100110011001100110011001100110011001100110011010₂ `ulp(float(0.1)) = 0.0000000000000000000000000000000
Floating-point decimalen optellen voor het plezier en het gewin
Een beroemd voorbeeld is dat bij IEEE double-precision floats 0.1 + 0.2 niet 0.3 is, maar 0.30000000000000004.
Aan de andere kant gebruik ik constant een Python REPL om decimale getallen op bonnetjes op te tellen. Natuurlijk zijn de belangen bij mij lager; ik tel bedragen op in de range van $1 tot $100 en weet dat ik de microscopische fouten handmatig moet afronden voordat ik de som ergens kopieer. Maar eigenlijk gebeurt het heel vaak dat die fouten helemaal niet optreden.
Als we elk paar veelvouden van 0,01 optellen tot 1,00 en kijken of het geprinte resultaat te groot (blauw), te klein (rood) of correct is, ontstaat er een interessant patroon:
Afbeelding 1: Hoe floating-point invloed heeft op het optellen van twee veelvouden van 0,01, tot 1,00
Waar komt dit patroon vandaan?
Floats in het kort
Een floating-point getal met dubbele precisie ($x$) bestaat uit 1 tekenbit, 11 exponentbits en 52 fractiebits (in die volgorde), voor een totaal van 64 bits.
De tekenbit bepaalt het teken, $+$ of $-$. De exponentbits representeren een geheel getal $E$ tussen $-1022$ en $1023$, inclusief. De fractiebits representeren een niet-negatief geheel getal $F$ kleiner dan $2^{52}$, wat correspondeert met de significand $1 + F/2^{52} \in [1, 2)$; die altijd aanwezige $1$ wordt de "hidden bit" genoemd. De waarde van het floating-point getal is $x = \pm 2^E(1 + F/2^{52})$.
De effectieve waarde van de laatste fractiebit is dus $2^{E-52}$, een hoeveelheid die de ulp wordt genoemd, voor "unit in the last place", van $x$. De twee floating-point getallen die het dichtst bij $x$ liggen, verschillen er precies één ulp van, behalve in een randgeval aan één zijde wanneer $F = 0$ en $x$ precies een macht van twee is.
Voorbeeld: float(0.1) = 0.00011001100110011001100110011001100110011001100110011010₂ `ulp(float(0.1)) = 0.0000000000000000000000000000000