15. Gleitkommaarithmetik: Probleme und Einschränkungen

Gleitkommazahlen werden in der Computerhardware als Brüche zur Basis 2 (binär) dargestellt. Zum Beispiel hat der dezimale Bruch 0.625 den Wert 6/10 + 2/100 + 5/1000, und auf die gleiche Weise hat der binäre Bruch 0.101 den Wert 1/2 + 0/4 + 1/8. Diese beiden Brüche haben identische Werte, der einzige wirkliche Unterschied besteht darin, dass der erste in Dezimalbruchschreibweise und der zweite in Binärschreibweise geschrieben ist.

Leider können die meisten Dezimalbrüche nicht exakt als Binärbrüche dargestellt werden. Eine Folge davon ist, dass die dezimalen Gleitkommazahlen, die Sie eingeben, im Allgemeinen nur durch die tatsächlich im Rechner gespeicherten binären Gleitkommazahlen angenähert werden.

Das Problem ist zunächst leichter in Basis 10 zu verstehen. Betrachten Sie den Bruch 1/3. Sie können diesen als Dezimalbruch annähern

0.3

oder besser,

0.33

oder besser,

0.333

und so weiter. Egal wie viele Ziffern Sie aufschreiben möchten, das Ergebnis wird niemals exakt 1/3 sein, sondern eine immer bessere Annäherung an 1/3.

Auf die gleiche Weise kann der Dezimalwert 0.1, egal wie viele Binärziffern Sie verwenden möchten, nicht exakt als Binärbruch dargestellt werden. In Basis 2 ist 1/10 der unendlich wiederkehrende Bruch

0.0001100110011001100110011001100110011001100110011...

Stoppen Sie bei einer beliebigen endlichen Anzahl von Bits, und Sie erhalten eine Annäherung. Auf den meisten heutigen Rechnern werden Gleitkommazahlen mit einem Binärbruch angenähert, wobei der Zähler die ersten 53 Bits ab der höchstwertigen Stelle verwendet und der Nenner eine Zweierpotenz ist. Im Fall von 1/10 ist der Binärbruch 3602879701896397 / 2 ** 55, was nahe am, aber nicht exakt gleich dem tatsächlichen Wert von 1/10 ist.

Viele Benutzer sind sich der Annäherung nicht bewusst, da die Werte so angezeigt werden. Python gibt nur eine Dezimalannäherung des tatsächlichen Dezimalwerts der binären Annäherung aus, die im Rechner gespeichert ist. Auf den meisten Rechnern müsste Python, wenn es den tatsächlichen Dezimalwert der binären Annäherung für 0.1 anzeigen würde, Folgendes ausgeben:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Das sind mehr Ziffern, als die meisten Leute nützlich finden, also hält Python die Anzahl der Ziffern überschaubar, indem es stattdessen einen gerundeten Wert anzeigt.

>>> 1 / 10
0.1

Denken Sie einfach daran, dass der tatsächlich gespeicherte Wert die nächstgelegene darstellbare Binärbruch ist, auch wenn das angezeigte Ergebnis wie der exakte Wert von 1/10 aussieht.

Interessanterweise gibt es viele verschiedene Dezimalzahlen, die denselben nächstgelegenen ungefähren Binärbruch gemeinsam haben. Zum Beispiel werden die Zahlen 0.1 und 0.10000000000000001 und 0.1000000000000000055511151231257827021181583404541015625 alle durch 3602879701896397 / 2 ** 55 angenähert. Da all diese Dezimalwerte die gleiche Annäherung teilen, könnte jede von ihnen angezeigt werden, während die Invariante eval(repr(x)) == x erhalten bleibt.

Historisch gesehen wählte die Python-Eingabeaufforderung und die eingebaute Funktion repr() diejenige mit 17 signifikanten Ziffern, 0.10000000000000001. Seit Python 3.1 ist Python (auf den meisten Systemen) in der Lage, die kürzeste dieser auszuwählen und einfach 0.1 anzuzeigen.

Beachten Sie, dass dies in der Natur der binären Gleitkommaarithmetik liegt: Dies ist kein Fehler in Python und auch kein Fehler in Ihrem Code. Sie werden das Gleiche in allen Sprachen sehen, die die Gleitkommaarithmetik Ihrer Hardware unterstützen (obwohl einige Sprachen den Unterschied möglicherweise nicht standardmäßig oder in allen Ausgabearten *anzeigen*).

Für eine angenehmere Ausgabe möchten Sie möglicherweise die String-Formatierung verwenden, um eine begrenzte Anzahl von signifikanten Ziffern zu erzeugen.

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'

>>> repr(math.pi)
'3.141592653589793'

Es ist wichtig zu erkennen, dass dies in einem echten Sinne eine Illusion ist: Sie runden lediglich die *Anzeige* des tatsächlichen Maschinenwerts.

Eine Illusion kann eine andere erzeugen. Zum Beispiel, da 0.1 nicht exakt 1/10 ist, ergibt die Summe von drei Werten von 0.1 möglicherweise auch nicht exakt 0.3.

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Da 0.1 dem exakten Wert von 1/10 nicht näher kommen kann und 0.3 dem exakten Wert von 3/10 nicht näher kommen kann, kann das Vorabrunden mit der Funktion round() nicht helfen.

>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
False

Obwohl die Zahlen den beabsichtigten exakten Werten nicht näher gebracht werden können, kann die Funktion math.isclose() nützlich sein, um ungenaue Werte zu vergleichen.

>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
True

Alternativ kann die Funktion round() verwendet werden, um grobe Annäherungen zu vergleichen.

>>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)
True

Die binäre Gleitkommaarithmetik birgt viele solcher Überraschungen. Das Problem mit "0.1" wird unten im Detail im Abschnitt "Darstellungsfehler" erklärt. Sehen Sie sich Examples of Floating Point Problems für eine angenehme Zusammenfassung an, wie binäre Gleitkommaarithmetik funktioniert und welche Probleme in der Praxis häufig auftreten. Siehe auch The Perils of Floating Point für eine vollständigere Darstellung anderer üblicher Überraschungen.

Wie dort am Ende gesagt wird, gibt es "keine einfachen Antworten". Seien Sie dennoch nicht übermäßig vorsichtig mit Gleitkommazahlen! Die Fehler bei Python-Gleitkommaoperationen stammen von der Gleitkommahardware und sind auf den meisten Rechnern pro Operation in der Größenordnung von nicht mehr als 1 Teil von 2**53. Das ist für die meisten Aufgaben mehr als ausreichend, aber Sie müssen bedenken, dass es sich nicht um Dezimalarithmetik handelt und jede Gleitkommaoperation einen neuen Rundungsfehler erleiden kann.

Obwohl pathologische Fälle existieren, werden Sie bei den meisten gelegentlichen Verwendungen der Gleitkommaarithmetik am Ende das erwartete Ergebnis sehen, wenn Sie die Anzeige Ihrer Endergebnisse einfach auf die erwartete Anzahl von Dezimalstellen runden. str() reicht normalerweise aus, und für feinere Kontrolle siehe die Formatierungs-Spezifizierer der Methode str.format() in Format String Syntax.

Für Anwendungsfälle, die eine exakte Dezimaldarstellung erfordern, versuchen Sie, das Modul decimal zu verwenden, das eine Dezimalarithmetik implementiert, die für Buchhaltungsanwendungen und Anwendungen mit hoher Präzision geeignet ist.

Eine weitere Form der exakten Arithmetik wird vom Modul fractions unterstützt, das eine Arithmetik basierend auf rationalen Zahlen implementiert (sodass Zahlen wie 1/3 exakt dargestellt werden können).

Wenn Sie ein intensiver Benutzer von Gleitkommaoperationen sind, sollten Sie sich das NumPy-Paket und viele andere Pakete für mathematische und statistische Operationen des SciPy-Projekts ansehen. Siehe <https://scipy.de>.

Python bietet Werkzeuge, die in den seltenen Fällen helfen können, in denen Sie wirklich den exakten Wert eines Gleitkommazahl wissen *möchten*. Die Methode float.as_integer_ratio() drückt den Wert einer Gleitkommazahl als Bruch aus.

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

Da das Verhältnis exakt ist, kann es verwendet werden, um den ursprünglichen Wert verlustfrei wiederherzustellen.

>>> x == 3537115888337719 / 1125899906842624
True

Die Methode float.hex() drückt eine Gleitkommazahl in Hexadezimal (Basis 16) aus und gibt ebenfalls den vom Computer gespeicherten exakten Wert an.

>>> x.hex()
'0x1.921f9f01b866ep+1'

Diese präzise hexadezimale Darstellung kann verwendet werden, um den Gleitkommawert exakt zu rekonstruieren.

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

Da die Darstellung exakt ist, ist sie nützlich für die zuverlässige Portierung von Werten über verschiedene Python-Versionen hinweg (Plattformunabhängigkeit) und den Datenaustausch mit anderen Sprachen, die dasselbe Format unterstützen (wie Java und C99).

Ein weiteres hilfreiches Werkzeug ist die Funktion sum(), die hilft, Präzisionsverluste während der Summierung zu mildern. Sie verwendet erweiterte Präzision für Zwischenrundungsschritte, während Werte zu einer laufenden Summe hinzugefügt werden. Das kann die Gesamtgenauigkeit verbessern, sodass sich die Fehler nicht bis zu dem Punkt anhäufen, an dem sie die Endsumme beeinflussen.

>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
False
>>> sum([0.1] * 10) == 1.0
True

Die Funktion math.fsum() geht weiter und verfolgt alle "verlorenen Ziffern", während Werte zu einer laufenden Summe hinzugefügt werden, sodass das Ergebnis nur eine einzige Rundung hat. Dies ist langsamer als sum(), ist aber in seltenen Fällen genauer, in denen stark voneinander abweichende Eingaben sich größtenteils gegenseitig aufheben und eine endgültige Summe nahe Null hinterlassen.

>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,
...        -143401161400469.7, 266262841.31058735, -0.003244936839808227]
>>> float(sum(map(Fraction, arr)))   # Exact summation with single rounding
8.042173697819788e-13
>>> math.fsum(arr)                   # Single rounding
8.042173697819788e-13
>>> sum(arr)                         # Multiple roundings in extended precision
8.042178034628478e-13
>>> total = 0.0
>>> for x in arr:
...     total += x                   # Multiple roundings in standard precision
...
>>> total                            # Straight addition has no correct digits!
-0.0051575902860057365

15.1. Darstellungsfehler

Dieser Abschnitt erklärt das Beispiel "0.1" im Detail und zeigt, wie Sie eine exakte Analyse solcher Fälle selbst durchführen können. Grundlegende Kenntnisse der binären Gleitkommadarstellung werden vorausgesetzt.

Darstellungsfehler bezieht sich auf die Tatsache, dass einige (tatsächlich die meisten) Dezimalbrüche nicht exakt als binäre (Basis 2) Brüche dargestellt werden können. Dies ist der Hauptgrund, warum Python (oder Perl, C, C++, Java, Fortran und viele andere) oft nicht die exakte Dezimalzahl anzeigt, die Sie erwarten.

Warum ist das so? 1/10 ist nicht exakt als Binärbruch darstellbar. Seit mindestens 2000 verwenden fast alle Rechner IEEE 754 binäre Gleitkommaarithmetik, und fast alle Plattformen ordnen Python-Gleitkommazahlen IEEE 754 binär64 "doppelter Genauigkeit" zu. IEEE 754 binär64-Werte enthalten 53 Bit Genauigkeit, sodass der Computer beim Eingeben bestrebt ist, 0.1 in den nächstgelegenen Bruch der Form *J*/2**N umzuwandeln, wobei *J* eine ganze Zahl mit genau 53 Bit ist. Umformung

1 / 10 ~= J / (2**N)

als

J ~= 2**N / 10

und unter der Erinnerung, dass *J* genau 53 Bit hat (also >= 2**52, aber < 2**53), ist der beste Wert für *N* 56.

>>> 2**52 <=  2**56 // 10  < 2**53
True

Das heißt, 56 ist der einzige Wert für *N*, der *J* genau 53 Bit lässt. Der bestmögliche Wert für *J* ist dann dieser Quotient, gerundet.

>>> q, r = divmod(2**56, 10)
>>> r
6

Da der Rest mehr als die Hälfte von 10 beträgt, wird die beste Annäherung durch Aufrunden erzielt.

>>> q+1
7205759403792794

Daher ist die bestmögliche Annäherung an 1/10 in IEEE 754 doppelter Genauigkeit:

7205759403792794 / 2 ** 56

Durch Division von Zähler und Nenner durch zwei wird der Bruch reduziert zu:

3602879701896397 / 2 ** 55

Beachten Sie, dass dies, da wir aufgerundet haben, tatsächlich etwas größer als 1/10 ist; wenn wir nicht aufgerundet hätten, wäre der Quotient etwas kleiner als 1/10 gewesen. Aber in keinem Fall kann es *exakt* 1/10 sein!

Der Computer "sieht" also niemals 1/10: Was er sieht, ist der exakte Bruch, der oben angegeben ist, die bestmögliche IEEE 754 Double-Annäherung, die er erhalten kann.

>>> 0.1 * 2 ** 55
3602879701896397.0

Wenn wir diesen Bruch mit 10**55 multiplizieren, können wir den Wert bis auf 55 Dezimalstellen sehen.

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

Das bedeutet, dass die exakte im Computer gespeicherte Zahl gleich dem Dezimalwert 0.1000000000000000055511151231257827021181583404541015625 ist. Anstatt den vollständigen Dezimalwert anzuzeigen, runden viele Sprachen (einschließlich älterer Python-Versionen) das Ergebnis auf 17 signifikante Ziffern.

>>> format(0.1, '.17f')
'0.10000000000000001'

Die Module fractions und decimal machen diese Berechnungen einfach.

>>> from decimal import Decimal
>>> from fractions import Fraction

>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)

>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'