FĂŒhren Sie 0.1 + 0.2 in einer Python-Shell aus, kommt nicht 0.3 heraus, sondern 0.30000000000000004. Probieren Sie es in JavaScript, Java, Go, C oder Rust, und Sie bekommen denselben sechzehnstelligen Schwanz. Jede dieser Sprachen tut genau das, was der Hardware-Standard vorschreibt. Eine Zahl, die unendlich viele BinĂ€rstellen brĂ€uchte, wird in 64 Bit gepresst, und irgendetwas muss nachgeben.

Warum sich 0.1 nicht exakt binÀr speichern lÀsst

Computer speichern Gleitkommazahlen zur Basis 2. In Basis 2 hat der Bruch 1/10 eine periodische Entwicklung, genau wie sich 1/3 in Basis 10 endlos wiederholt (0.3333...). Ausgeschrieben beginnt 0.1 binÀr so:

1
0.1₁₀ = 0.000110011001100110011...₂  (wiederholt sich endlos)

Keine endliche Anzahl Bits schneidet diese Folge so ab, dass genau 0.1 herauskommt. Ein 64-Bit-„Double" nach IEEE 754 (1 Vorzeichenbit, 11 Exponentenbits, 52 gespeicherte Mantissenbits, 53 Bit Genauigkeit mit der impliziten fĂŒhrenden 1) rundet die unendliche Entwicklung auf den nĂ€chsten darstellbaren Wert, der immer ein Bruch der Form J / 2^N ist. FĂŒr 0.1 ist dieser Wert:

1
3602879701896397 / 2^55

Als Dezimalzahl ist das 0.1000000000000000055511151231257827021181583404541015625. Python zeigt es Ihnen. Jedes Codebeispiel in diesem Artikel nutzt nur die Standardbibliothek. Sie brauchen also keine virtuelle Umgebung, um mitzumachen:

1
2
3
4
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
>>> 0.1 == 0.1000000000000000055511151231257827021181583404541015625
True

0.2 bringt seinen eigenen, leicht anderen Rundungsfehler mit. Addieren Sie die beiden schon unperfekten Werte, dann heben sich die Fehler nicht auf, sondern summieren sich zu einem Ergebnis, das einen Schritt ĂŒber dem Double liegt, das 0.3 am nĂ€chsten kommt. Daher 0.30000000000000004.

Welche Zahlen Gleitkomma wirklich exakt speichert

Nicht jede Dezimalzahl ist verloren. Ein BinĂ€rbruch endet, wenn sein Nenner, vollstĂ€ndig gekĂŒrzt, eine Zweierpotenz ist. 0.5 ist 1/2. 0.25 ist 1/4. 0.125 ist 1/8. Die sind exakt:

1
2
3
4
>>> 0.5 + 0.25 == 0.75
True
>>> 0.1 + 0.2 == 0.3
False

Die Dezimalzahlen, die Menschen tatsÀchlich eintippen, etwa Preise und ProzentsÀtze, haben fast nie einen Nenner, der eine Zweierpotenz ist. Das macht den Fehler bei 0.1 + 0.2 zum Normalfall von Dezimal-Arithmetik auf binÀren Floats, und er summiert sich. Addieren Sie 0.1 zehnmal in irgendeiner Sprache, die Doubles verwendet, so landen Sie nicht bei 1.0:

1
2
3
4
5
>>> total = 0.0
>>> for _ in range(10):
...     total += 0.1
>>> total
0.9999999999999999

Zehn Additionen reichen dafĂŒr. Beachten Sie: Die Big-O-Notation sagt, wie schnell diese Schleife mit der EingabegrĂ¶ĂŸe wĂ€chst, aber nichts darĂŒber, ob das Ergebnis stimmt – die Korrektheit ist hier eine eigene PrĂŒfung, die Sie selbst vornehmen mĂŒssen.

Wie Sie Gleitkommazahlen sicher vergleichen

Da == bei Floats nicht zuverlĂ€ssig ist, vergleichen Sie mit einer Toleranz: PrĂŒfen Sie, ob zwei Zahlen nah genug beieinander liegen, statt identisch zu sein. Jede verbreitete Sprache hat dafĂŒr ihr eigenes Idiom.

SpracheSicherer Vergleich
Pythonmath.isclose(a, b) — relative Toleranz standardmĂ€ĂŸig 1e-9
JavaScriptMath.abs(a - b) < Number.EPSILON * Skala
JavaMath.abs(a - b) < 1e-9, oder BigDecimal fĂŒr exaktes Dezimal
C#Math.Abs(a - b) < 1e-9, oder der native Typ decimal
Gomath.Abs(a-b) <= Toleranz (kein eingebauter Helfer)

In Python:

1
2
3
4
5
>>> import math
>>> 0.1 + 0.1 + 0.1 == 0.3
False
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
True

math.isclose skaliert seine Toleranz an die GrĂ¶ĂŸe der Zahlen (rel_tol * max(abs(a), abs(b))), sodass derselbe Aufruf bei kleinen und großen GrĂ¶ĂŸenordnungen funktioniert. Der Standardwert fĂŒr abs_tol ist allerdings 0.0. Kann einer der beiden Werte legitim null oder sehr nah daran sein, ĂŒbergeben Sie ein explizites abs_tol – sonst schlĂ€gt der Vergleich bei Werten fehl, die Sie fĂŒr gleich halten wĂŒrden.

Number.EPSILON in JavaScript (2⁻⁔ÂČ, etwa 2.22e-16) ist der Abstand zwischen 1 und dem nĂ€chsten darstellbaren Double, also fĂŒr sich allein nur bei Zahlen nahe 1 ein gĂŒltiger Schwellenwert. Skalieren Sie ihn fĂŒr alles andere:

1
2
3
4
5
function nearlyEqual(x, y, scale = 1) {
  return Math.abs(x - y) < Number.EPSILON * scale;
}
nearlyEqual(0.1 + 0.2, 0.3);              // true
nearlyEqual(1000.1 + 1000.2, 2000.3, 2000); // true, die Skala passt zur GrĂ¶ĂŸenordnung

Eine Assertion wie result == 0.3 in einem Test ist ein Bug, der nur auf eine andere Eingabe, Plattform oder einen anderen Compiler wartet, um sichtbar zu werden. Ob Unit-Test oder weiter oben in der Testpyramide – ersetzen Sie den exakten Vergleich durch eine ToleranzprĂŒfung.

Wenn Gleitkomma das falsche Werkzeug ist: Geld und ZĂ€hlwerte

Eine Toleranz behebt den Vergleich. Sie behebt nicht die Speicherung. Wenn Sie Preise aufsummieren oder einen laufenden Kontostand fĂŒhren, ist der Rundungsfehler selbst das Problem, und die Lösung ist, fĂŒr diesen Wert kein float/double mehr zu verwenden.

Die einfachste Variante: Geld als Ganzzahl der kleinsten Einheit speichern. 19,99 € sind die Ganzzahl 1999 Cent, nicht der Float 19.99. Addition und Subtraktion zwischen Ganzzahlen haben keinen Rundungsfehler. Wandeln Sie erst beim Anzeigen des Werts in einen Dezimalstring um.

Brauchen Sie echte DezimalbrĂŒche mit exakten Rundungsregeln (eine Buchhaltung, eine Steuerberechnung, alles, was auf den Cent aufgehen muss), nutzen Sie einen Dezimaltyp:

1
2
3
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
True

Java hat dafĂŒr BigDecimal; C# hat einen nativen 128-Bit-Typ decimal mit etwa 28-29 signifikanten Dezimalstellen. Alle drei speichern Ziffern zur Basis 10, sodass die DezimalbrĂŒche, die Menschen schreiben, ohne Rundung gespeichert werden. DafĂŒr zahlen Sie mit Geschwindigkeit und UmstĂ€ndlichkeit. Dezimaltypen sind langsamer als native Floats und brauchen bei der Division einen expliziten Rundungsmodus. FĂŒr einen Bankkontostand lohnt sich das. FĂŒr eine zahlenintensive Schleife ist es ein schlechter Tausch.

Wenn die Ungenauigkeit von Gleitkomma keine Rolle spielt

Grafiktransformationen, physikalische Simulationen, Gewichte von Machine-Learning-Modellen und Audioverarbeitung laufen absichtlich auf Floats. Ein relativer Fehler um 1e-16 liegt weit unter dem, was diese Bereiche ĂŒberhaupt bemerken könnten, und native Float-Hardware ist deutlich schneller als Dezimal-Arithmetik.

Die Frage zu einer Zahl ist also nicht, ob sie ein Komma hat. Fragen Sie sich, ob Sie diskrete, exakte Dinge zĂ€hlen, etwa Geld oder das erwartete Ergebnis eines Tests, oder eine kontinuierliche GrĂ¶ĂŸe messen, die von Anfang an ungefĂ€hr war, etwa eine Pixelposition oder einen Sensorwert. GezĂ€hlte Werte brauchen Ganzzahlen oder einen Dezimaltyp. Gemessene Werte sind genau das, wofĂŒr Gleitkomma gebaut wurde, und sie auf Decimal umzustellen macht sie nur langsamer, ohne sie nennenswert korrekter zu machen.

Wenn Sie das nĂ€chste Mal ein als float deklariertes Saldofeld sehen oder einen Test, der result == 0.3 behauptet, entscheiden Sie, um welche Art Zahl es sich handelt, und korrigieren Sie den Speichertyp oder den Vergleich gleich in dieser Review – bevor ein fehlender Cent in der Produktion auftaucht.

Verwandte Artikel