Der Gleitkomma-Präzisionsfehler, den jeder trifft
Öffne eine beliebige Programmiersprache und tippe 0.1 + 0.2. Du bekommst 0.30000000000000004. Nicht 0.3. Das ist kein Bug in JavaScript, Python, Java, C++ oder Rust — es ist eine fundamentale Konsequenz davon, wie Computer Dezimalbrüche binär darstellen. Die Gleitkomma-Präzisionsfehler, die du siehst, sind keine Fehler; sie sind das mathematisch korrekte Ergebnis binärer Arithmetik auf angenäherten Werten.
Der Grund: 0.1 dezimal ist ein periodischer Bruch binär (0.0001100110011...), genau wie 1/3 dezimal periodisch ist (0.333...). Ein 64-Bit-Float kann nur 53 signifikante Bits speichern, also wird 0.1 auf den nächsten darstellbaren Wert gerundet: 0.1000000000000000055511151231257827021181583404541015625. Wenn du zwei dieser Näherungen addierst, heben sich die Rundungsfehler nicht auf — sie akkumulieren sich.
Das betrifft jede Sprache, die IEEE-754-Gleitkomma verwendet (also alle für hardwarebeschleunigte Mathematik). Python, JavaScript, C, Java, Go, Rust — alle produzieren das gleiche Ergebnis, weil sie alle das gleiche 64-Bit-Double-Precision-Format verwenden. Die einzigen Sprachen, die das standardmäßig vermeiden, sind solche mit Arithmetik beliebiger Präzision (wie Haskells Rational-Typ oder Pythons Decimal-Modul), die Geschwindigkeit gegen Exaktheit tauschen.
Wie IEEE 754 Zahlen darstellt
IEEE-754-Double-Precision-(64-Bit)-Floats speichern Zahlen als: 1 Vorzeichenbit, 11 Exponentenbits und 52 Mantissenbits (plus 1 implizites führendes Bit = 53 Bits Präzision). Der Wert ist: (-1)^Vorzeichen × 2^(Exponent-1023) × 1.Mantisse. Das gibt dir etwa 15-17 signifikante Dezimalstellen Präzision und einen Bereich von ±5×10^-324 bis ±1,8×10^308.
Die 53 Bits Mantisse bedeuten, dass du Ganzzahlen exakt bis 2^53 (9.007.199.254.740.992) darstellen kannst. Darüber hinaus sind aufeinanderfolgende darstellbare Zahlen mehr als 1 auseinander. In JavaScript: 9007199254740992 + 1 === 9007199254740992 ergibt true. Die Zahl kann buchstäblich nicht dargestellt werden. Deshalb hat JavaScript BigInt und deshalb müssen Datenbank-IDs über 2^53 als Strings in JSON übertragen werden.
Zwischen zwei beliebigen darstellbaren Floats gibt es unendlich viele reelle Zahlen, die nicht dargestellt werden können. Der Abstand zwischen darstellbaren Zahlen variiert — nahe Null sind aufeinanderfolgende Floats etwa 5×10^-324 auseinander. Nahe 1,0 sind sie etwa 1,1×10^-16 auseinander. Nahe 1.000.000 sind sie etwa 1,2×10^-10 auseinander. Das bedeutet, die relative Präzision ist ungefähr konstant (~15 Stellen), aber die absolute Präzision nimmt ab, wenn Zahlen größer werden.
Spezialwerte: +0 und -0 (ja, negative Null existiert und -0 === +0 in JavaScript), Infinity und -Infinity (Ergebnis von Overflow oder Division durch Null) und NaN (Not a Number — Ergebnis von 0/0, sqrt(-1) oder ungültigen Operationen). NaN hat die bizarre Eigenschaft, dass NaN !== NaN — es ist der einzige Wert in IEEE 754, der nicht gleich sich selbst ist. Verwende Number.isNaN() oder isNaN() zur Prüfung.
// The classic floating point precision errors
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
1.0 - 0.9 // 0.09999999999999998
0.1 * 0.2 // 0.020000000000000004
// Large integer precision loss
9007199254740992 + 1 // 9007199254740992 (unchanged!)
9007199254740993 === 9007199254740992 // true (!)
// Special values
1 / 0 // Infinity
-1 / 0 // -Infinity
0 / 0 // NaN
NaN === NaN // false (!)
Number.isNaN(NaN) // true
// The epsilon approach for comparison
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
// But EPSILON is too small for accumulated errors:
Math.abs(0.1 + 0.2 + 0.3 - 0.6) < Number.EPSILON // false!Gleitkomma und Geld (verwende keine Floats)
Verwende nie Gleitkomma für Finanzberechnungen. 0,10 € + 0,20 € sollte 0,30 € ergeben, nicht 0,30000000000000004 €. Über Tausende von Transaktionen akkumulieren sich diese winzigen Fehler zu echten Diskrepanzen. Eine Bank, die 10 Millionen Transaktionen pro Tag verarbeitet mit Rundungsfehlern von ±0,000000000000001 pro Transaktion, könnte um 0,01 € pro Tag abdriften — was Audit-Fehler und regulatorische Probleme auslöst.
Die Standardlösung: Speichere Geld als Ganzzahlen in der kleinsten Einheit (Cent). 19,99 € wird zu 1999 Cent. Alle Arithmetik ist exakte Ganzzahl-Mathematik. Konvertiere nur zur Anzeige in Dezimal. So funktionieren Stripe, PayPal und jedes seriöse Zahlungssystem. In JavaScript: const total = priceInCents * quantity; const display = (total / 100).toFixed(2).
Für Währungen mit Sub-Cent-Präzision (Kryptowährung, Forex) verwende eine Fixed-Point-Bibliothek. JavaScript hat keinen eingebauten Dezimaltyp, aber Bibliotheken wie decimal.js, big.js oder dinero.js handhaben Dezimalarithmetik beliebiger Präzision. Python hat das decimal-Modul (from decimal import Decimal; Decimal("0.1") + Decimal("0.2") == Decimal("0.3") ist True). Java hat BigDecimal. Verwende diese für jede Berechnung, wo exakte Dezimalergebnisse wichtig sind.
Ein Beispiel aus der Praxis: 2012 verlor Knight Capital 440 Millionen Dollar in 45 Minuten durch einen Software-Bug. Obwohl es streng genommen kein Gleitkomma-Fehler war, illustriert es, wie kleine Berechnungsfehler in Finanzsystemen katastrophal kaskadieren. Die Lektion: Finanzcode braucht exakte Arithmetik, ausgiebiges Testen und Sicherheitsmechanismen. Unser percentage-calculator verwendet intern Ganzzahl-Arithmetik, um diese Probleme bei der Berechnung von Steuersätzen und Rabatten zu vermeiden.
Gleitkommazahlen vergleichen
Verwende nie === (oder ==) zum Vergleichen von Gleitkomma-Ergebnissen. Prüfe stattdessen, ob die Differenz kleiner als eine Toleranz ist: Math.abs(a - b) < epsilon. Aber wie groß sollte epsilon sein? Number.EPSILON (2,2×10^-16) ist die kleinste Differenz zwischen 1,0 und dem nächsten darstellbaren Float — es ist angemessen für den Vergleich von Zahlen nahe 1,0, aber zu klein für größere Zahlen und zu groß für Zahlen nahe Null.
Ein besserer Ansatz: Relatives Epsilon. Vergleiche die Differenz mit der Größenordnung der Zahlen: Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)). Das skaliert die Toleranz mit der Größe der Werte. Für die meisten Anwendungen funktioniert ein relatives Epsilon von 1e-9 bis 1e-12 gut. Für akkumulierte Berechnungen (Summierung Tausender Werte) brauchst du vielleicht 1e-6.
Der ULP-Ansatz (Unit in the Last Place): Zwei Floats sind "gleich", wenn sie sich um höchstens N ULPs unterscheiden. Ein ULP ist der Abstand zwischen einem Float und seinem nächsten Nachbarn. Das ist der mathematisch rigoroseste Vergleich, aber schwieriger zu implementieren. In der Praxis deckt der relative Epsilon-Vergleich 99% der Anwendungsfälle ab.
Für Sortierung und Deduplizierung ist der Gleitkomma-Vergleich besonders knifflig. Wenn du "doppelte" Werte aus einer Liste von Messungen entfernst, brauchst du eine konsistente Äquivalenzrelation (wenn a≈b und b≈c, dann a≈c). Einfacher Epsilon-Vergleich garantiert diese Transitivität nicht. Erwäge, vor dem Vergleich auf eine feste Anzahl signifikanter Stellen zu runden, oder verwende Ganzzahl-Darstellungen.
Wissenschaftliches Rechnen: Wenn Präzision am meisten zählt
Katastrophale Auslöschung: Das Subtrahieren zweier fast gleicher Zahlen verliert die meisten signifikanten Stellen. Wenn a = 1,0000001 und b = 1,0000000 (beide mit 15 Stellen Präzision gespeichert), hat a - b = 0,0000001 nur 1 Stelle Präzision — die anderen 14 Stellen sind Rauschen. Das passiert bei Berechnungen der quadratischen Formel, numerischen Ableitungen und jedem Algorithmus, der kleine Differenzen großer Zahlen berechnet.
Die Lösung für die quadratische Formel: Statt x = (-b ± sqrt(b²-4ac)) / 2a verwende die numerisch stabile Form. Berechne eine Wurzel mit der Standardformel (wähle das Vorzeichen, das Auslöschung vermeidet), dann verwende x1 * x2 = c/a für die andere Wurzel. Das vermeidet die katastrophale Auslöschung, die auftritt wenn b² >> 4ac. Unser scientific-calculator verwendet diese stabilisierte Form intern.
Die Summierungsreihenfolge zählt. 1e-16 zu 1,0 addieren ergibt 1,0 (der kleine Wert liegt unter der Präzisionsschwelle). Aber 1e-16 eine Billion Mal addieren sollte 0,0001 ergeben. Naive Summierung verliert diese kleinen Beiträge komplett. Kahan-Summierung (auch kompensierte Summierung genannt) verfolgt den Rundungsfehler und korrigiert ihn, gibt Ergebnisse mit der vollen Präzision des Floats. Verwende sie, wenn du viele Werte unterschiedlicher Größenordnung summierst.
Intervall-Arithmetik: Statt einen einzelnen Float zu berechnen, verfolge den Bereich [untere, obere Grenze], der das wahre Ergebnis enthält. Nach jeder Operation weitet sich das Intervall, um Rundung zu berücksichtigen. Wenn dein finales Intervall [2,99999, 3,00001] ist, weißt du, dass die wahre Antwort 3,0 ± 0,00001 ist. Das gibt dir eine rigorose Fehlerschranke, anders als Single-Float-Berechnung, wo du rätst, wie viel Fehler sich akkumuliert hat. Bibliotheken wie MPFI (C) und pyinterval (Python) implementieren das.
Sprachspezifische Fallstricke
JavaScript: Alle Zahlen sind 64-Bit-Doubles (kein Integer-Typ bis BigInt). Das bedeutet, Ganzzahl-Arithmetik ist nur bis 2^53 exakt. JSON.parse() großer Integer verliert stillschweigend Präzision: JSON.parse("9007199254740993") gibt 9007199254740992 zurück. APIs, die große IDs zurückgeben (Twitter, Discord), müssen sie als Strings senden. Verwende BigInt für exakte Ganzzahl-Arithmetik jenseits von 2^53.
Python: float ist 64-Bit-Double. Aber Python hat auch Decimal (Dezimal beliebiger Präzision) und Fraction (exakte rationale Arithmetik). Für Finanzcode: from decimal import Decimal, ROUND_HALF_UP. Für wissenschaftlichen Code: numpy verwendet standardmäßig 64-Bit-Doubles, unterstützt aber float128 auf manchen Plattformen. Pythons //-Operator macht Floor-Division, die mit negativen Zahlen anders interagiert als Trunkierung.
C/C++: float ist 32-Bit (7 Stellen Präzision), double ist 64-Bit (15 Stellen), long double ist 80-Bit oder 128-Bit je nach Plattform. Die x87-FPU verwendet intern 80-Bit erweiterte Präzision, was bedeutet, dass der gleiche Code verschiedene Ergebnisse geben kann, je nachdem ob Zwischenwerte in Registern bleiben (80-Bit) oder in den Speicher geschrieben werden (64-Bit). Verwende -ffp-contract=off und -fno-fast-math für reproduzierbare Ergebnisse.
SQL: FLOAT und DOUBLE sind IEEE-754-Typen mit den gleichen Präzisionsproblemen. DECIMAL(p,s) und NUMERIC(p,s) sind exakte Fixed-Point-Typen — verwende diese für Geld. PostgreSQLs NUMERIC kann bis zu 131.072 Stellen vor dem Dezimalpunkt und 16.383 danach speichern. MySQLs DECIMAL unterstützt bis zu 65 Stellen insgesamt. Verwende immer DECIMAL für Finanzspalten, nie FLOAT oder DOUBLE.
Praktische Regeln zur Vermeidung von Gleitkomma-Bugs
Regel 1: Verwende Ganzzahlen für Geld. Speichere Cent, nicht Euro. Speichere Satoshis, nicht Bitcoin. Konvertiere nur zur Anzeige in Dezimal. Das eliminiert 90% der Gleitkomma-Bugs in Geschäftsanwendungen.
Regel 2: Vergleiche Floats nie mit ===. Verwende einen toleranzbasierten Vergleich, der für deine Domäne angemessen ist. Für UI-Koordinaten ist 0,01 Pixel Toleranz in Ordnung. Für wissenschaftliche Berechnungen verwende relatives Epsilon. Für Finanzberechnungen solltest du gar keine Floats verwenden (siehe Regel 1).
Regel 3: Sei misstrauisch bei Subtraktion. Wenn du zwei Zahlen subtrahierst, die nahe beieinander liegen könnten, riskierst du katastrophale Auslöschung. Formuliere den Algorithmus um, um die Subtraktion zu vermeiden, oder verwende höhere Präzision für diese spezifische Berechnung. Die quadratische Formel, numerische Ableitungen und Varianzberechnungen sind klassische Beispiele.
Regel 4: Teste mit adversarialen Eingaben. Werte nahe Null, sehr große Werte, Werte die exakte Zweierpotenzen sind (exakt darstellbar) vs Werte die es nicht sind (0.1, 0.3, 0.7). Summen vieler kleiner Werte. Differenzen fast gleicher Werte. Wenn dein Code Geld verarbeitet, teste mit 0,01 €, 0,10 €, 99,99 € und 999.999.999,99 €. Unser basic-calculator Tool ist darauf ausgelegt, diese Randfälle korrekt zu handhaben, indem es geeignete interne Darstellungen verwendet.