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:
| |
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:
| |
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:
| |
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:
| |
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:
| |
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.
| Sprache | Sicherer Vergleich |
|---|---|
| Python | math.isclose(a, b) â relative Toleranz standardmĂ€Ăig 1e-9 |
| JavaScript | Math.abs(a - b) < Number.EPSILON * Skala |
| Java | Math.abs(a - b) < 1e-9, oder BigDecimal fĂŒr exaktes Dezimal |
| C# | Math.Abs(a - b) < 1e-9, oder der native Typ decimal |
| Go | math.Abs(a-b) <= Toleranz (kein eingebauter Helfer) |
In Python:
| |
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:
| |
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:
| |
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.