Gleitkomma-Genauigkeit: Warum 0.1 + 0.2 ≠ 0.3 ist
0.1 + 0.2 liefert 0.30000000000000004, weil weder 0.1 noch 0.2 in einer binären Gleitkommazahl existiert. Ein Double kann nur Werte der Form m × 2ⁿ aufnehmen, und ein Zehntel ist zur Basis 2 ein unendlicher periodischer Bruch. Die Hardware behält deshalb den nächstgelegenen darstellbaren Nachbarn. Das Double hinter 0.1 ist exakt 0.1000000000000000055511151231257827021181583404541015625. Das hinter 0.2 ist 0.200000000000000011102230246251565404236316680908203125. Addieren Sie diese beiden gespeicherten Werte, landet die exakte Summe zwischen zwei darstellbaren Doubles. IEEE 754 rundet auf den näheren Wert, und der liegt eine Haaresbreite über 0.3.
Das ist die ganze Antwort. Die Fließkomma-Genauigkeit (auch Gleitkomma-Genauigkeit) ist endlich, Dezimalbrüche passen selten ins binäre Raster, und jede Operation rundet erneut.
Das ist keine JavaScript-Eigenart und kein CPU-Defekt. Dasselbe Ergebnis erscheint in Python, Java, C, Go, Rust, Swift und in den FLOAT-Spalten von SQL, denn sie alle laufen auf IEEE 754. Fügen Sie einen beliebigen Wert in den kostenlosen IEEE 754 Gleitkommazahlen-Konverter ein, um die exakten Bits und den exakt gespeicherten Wert Ziffer für Ziffer zu sehen.
Bleiben vier Fragen: warum der Fehler überhaupt entsteht, warum Ihre Sprache ihn manchmal verbirgt, wie Sie Floats ohne == vergleichen und welcher numerische Typ taugt, sobald Genauigkeit Geld kostet.
Gleitkomma-Genauigkeit in 60 Sekunden
In deutscher Schreibweise lautet die Rechnung 0,1 + 0,2; im Code und in diesem Artikel steht wie üblich der Punkt. An der Binärdarstellung ändert das Trennzeichen nichts.
Drei Fakten erklären fast jede Überraschung:
- Binäre Gleitkommazahlen können nur Zahlen der Form m × 2ⁿ darstellen, also Summen von Zweierpotenzen.
- Dezimalbrüche wie
0.1,0.2und0.3haben diese Form nicht und werden beim Einlesen gerundet. - Jede Rechenoperation rundet ihr Ergebnis erneut auf den nächstgelegenen darstellbaren Wert.
| Dezimalzahl | Exakt darstellbar? | Warum |
|---|---|---|
| 0.5 | Ja | 2⁻¹ |
| 0.25 | Ja | 2⁻² |
| 0.75 | Ja | 2⁻¹ + 2⁻² |
| 2.5 | Ja | 2 + 2⁻¹ |
| 100 | Ja | Jede ganze Zahl unter 2⁵³ |
| 0.1 | Nein | 1/10, der Nenner enthält den Faktor 5 |
| 0.2 | Nein | 1/5, gleicher Faktor |
| 0.3 | Nein | 3/10, gleicher Faktor |
Faustregel: Kürzen Sie den Bruch, und wenn der Nenner eine reine Zweierpotenz ist, ist der Wert exakt. Jeder verbleibende Faktor 5 bedeutet, dass der Wert nur näherungsweise gespeichert wird.
Warum 0.1 nicht exakt gespeichert werden kann
Die Bits nach dem Binärpunkt tragen die Wertigkeiten 1/2, 1/4, 1/8, 1/16, 1/32 und so weiter. Versuchen Sie, daraus 0.1 zusammenzusetzen, treffen Sie den Wert nie. Sie bekommen 1/16 = 0.0625, plus 1/32 = 0.09375, plus 1/256 = 0.09765625, jedes Mal näher, nie exakt. Binär ist ein Zehntel 0.0001100110011…, wobei sich 0011 endlos wiederholt.
Das Dezimalsystem hat dasselbe Problem mit anderen Brüchen. 1/3 zur Basis 10 ergibt 0.333…, und keine endliche Ziffernfolge trifft den Wert je genau. Niemand nennt das einen Fehler des Dezimalsystems. Die Basis 2 zieht die Grenze nur an einer anderen Stelle, und 1/10 liegt zufällig auf der falschen Seite. Der Abschnitt Floating Point Arithmetic: Issues and Limitations der Python-Dokumentation führt dieselbe Entwicklung vor, falls Sie die Langfassung möchten.
Binär ist einfach die Basis 2, und dieselbe Stellenwertarithmetik treibt Hexadezimal und Oktal an. Der Binär-, Hex-, Dezimal- & Oktalkonverter zeigt, wie ein Wert zwischen den Basen wandert, und unser Leitfaden zum Umrechnen von Zahlensystemen behandelt die Ganzzahlseite ausführlich. Weh tut die Basis 2 erst bei Brüchen.
Was ein Double tatsächlich speichert
Ein 64-Bit-Double zerfällt in drei Felder: 1 Vorzeichenbit, 11 Exponentenbits und 52 Mantissenbits. Die Mantisse trägt eine implizite führende 1, die nie mitgespeichert wird, sodass Sie 53 Bit Signifikand erhalten, also rund 15.95 Dezimalstellen an Gleitkomma-Genauigkeit. Den Exponenten legt das Format mit einem Bias von 1023 ab.
Fragen Sie eine beliebige Sprache nach mehr Stellen, als sie normalerweise ausgibt, und die Näherung wird sichtbar:
>>> 0.1
0.1
>>> f"{0.1:.20f}"
'0.10000000000000000555'
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
(0.1).toFixed(20); // '0.10000000000000000555'
(0.1).toPrecision(20); // '0.10000000000000000555'
Decimal(0.1) ist die ehrliche Sicht: Es konvertiert das Double, das Sie ohnehin schon haben, ohne erneut zu runden, und gibt alle 55 Stellen aus. Das Genauigkeitspanel im IEEE 754 Gleitkommazahlen-Konverter zeigt dieselbe Entwicklung für jede Zahl, die Sie eintippen, direkt neben dem vorzeichenbehafteten Rundungsfehler gegenüber Ihrer Eingabe.
Die exakte Arithmetik hinter 0.30000000000000004
Warum aber genau diese Zahl und nicht irgendein anderer Beinahetreffer?
Addieren Sie die beiden gespeicherten Werte exakt, ohne jede Rundung, kommt heraus:
0.1 → 0.1000000000000000055511151231257827021181583404541015625
0.2 → 0.200000000000000011102230246251565404236316680908203125
sum → 0.3000000000000000166533453693773481063544750213623046875
Diese Summe ist selbst kein darstellbares Double. Sie liegt zwischen zwei Nachbarn im binären Raster:
below: 0.299999999999999988897769753748434595763683319091796875 (prints as 0.3)
above: 0.3000000000000000444089209850062616169452667236328125 (prints as 0.30000000000000004)
Messen Sie nun die Abstände, und etwas Ungewöhnliches kommt zum Vorschein. Die exakte Summe liegt 2⁻⁵⁵ ≈ 2.776e-17 über dem unteren Nachbarn und 2⁻⁵⁵ unter dem oberen. Sie liegt also nicht näher an einem der beiden, sondern exakt in der Mitte: ein perfekter Gleichstand.
Runden auf den nächsten Wert hat hier keinen Abstand, mit dem es arbeiten könnte, also greift die Gleichstandsregel von IEEE 754: round half to even, gewinnt also der Kandidat, dessen letztes Mantissenbit 0 ist. Der Signifikand des unteren Nachbarn ist 5404319552844595 und damit ungerade. Der des oberen ist 5404319552844596 und damit gerade. Der gerade Wert gewinnt, ergibt das Bitmuster 0x3FD3333333333334, liegt ein ULP über dem Double, auf das das Literal 0.3 abgebildet wird, und erscheint als 0.30000000000000004.
Warum ausgerechnet gerade? Würde man Hälften immer aufrunden, verzöge das lange Summen in eine Richtung. Das abwechselnde Runden zum geraden Nachbarn hält diese Drift über viele Operationen nahe null. Es ist dasselbe kaufmännische Runden, das Buchhalter seit jeher benutzen, nur in Silizium standardisiert. Und es ist der Grund, warum der berühmteste Rundungsfehler des Internets ausgerechnet auf ...04 endet.
Stellen Sie dem eine Summe gegenüber, die überhaupt keine Rundung braucht:
0.5 + 0.25 === 0.75; // true
0.1 + 0.2 === 0.3; // false
0.5, 0.25 und 0.75 sind 2⁻¹, 2⁻² und 2⁻¹ + 2⁻². Alle drei sind exakt, ihre Summe ist exakt, und die Gleichheit verhält sich so, wie es die Schulmathematik nahelegt. Das Nachbarn-Panel des Konverters zeigt den vorherigen und den nächsten darstellbaren Wert rund um Ihre Eingabe samt ULP-Abstand, sodass Sie das Raster um 0.3 selbst prüfen können.
Warum Ihre Sprache den Fehler manchmal verbirgt
Genau das lässt das ganze Thema so gespenstisch wirken: 0.1 erscheint als 0.1, 0.1 + 0.2 dagegen mit siebzehn Stellen. Dasselbe Speicherformat, völlig unterschiedliche Ausgabe.
Moderne Laufzeitumgebungen verwenden die kürzeste Round-Trip-Formatierung: Sie geben die kürzeste Dezimalzeichenkette aus, die zurück auf dasselbe Double geparst wird. "0.1" bildet bereits eindeutig auf den für 0.1 gespeicherten Wert ab, also sehen Sie genau das. Die Summe 0.1 + 0.2 ist ein anderes Double als jenes, auf das "0.3" abbildet, deshalb muss der Formatierer so lange Stellen anhängen, bis die Zeichenkette eindeutig ist, und dafür braucht er alle siebzehn. Diese Technik geht auf David Gays Arbeit zum korrekten Runden von 1990 zurück; Grisu und später Ryu machten sie schnell genug für jede Standardbibliothek. Ihre Sprache lügt nicht. Sie zeigt die kürzeste Bezeichnung, die den Wert eindeutig identifiziert.
Verhalten je nach Sprache
| Sprache | Standard-Float-Typ | 0.1 + 0.2 erscheint als | Option für exakte Dezimalzahlen |
|---|---|---|---|
| JavaScript | number (nur Double) | 0.30000000000000004 | Nichts eingebaut; Integer-Cents oder eine Bibliothek |
| Python | float (Double) | 0.30000000000000004 | decimal.Decimal, fractions.Fraction |
| Java | double | 0.30000000000000004 | BigDecimal (aus einer Zeichenkette erzeugen) |
| C# | double | 0.30000000000000004 | decimal, 128 Bit zur Basis 10 |
| Go | float64 | 0.30000000000000004 (über Variablen) | math/big.Rat |
| Rust | f64 | 0.30000000000000004 | Crate rust_decimal |
| SQL | FLOAT / DOUBLE PRECISION | 0.30000000000000004 | NUMERIC / DECIMAL |
Zwei dieser Zeilen verbergen eine Falle.
Go faltet Konstanten mit beliebiger Genauigkeit. Ein Ausdruck aus Literalen wird zur Kompilierzeit exakt ausgewertet und erst danach konvertiert, sodass die Konstante 0.1 + 0.2 exakt 0.3 wird, noch bevor sie überhaupt ein float64 ist:
package main
import "fmt"
func main() {
fmt.Println(0.1 + 0.2) // 0.3 — constant folded exactly, then converted
a, b := 0.1, 0.2
fmt.Println(a + b) // 0.30000000000000004
fmt.Println(a+b == 0.3) // false
}
Javas BigDecimal erbt den Fehler, wenn Sie ihm ein Double übergeben. Der Konstruktor konvertiert getreu die Bits, die er bekommt:
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3
SQL unterscheidet nach Spaltentyp, nicht nach Dialekt:
-- PostgreSQL: a bare decimal literal is NUMERIC, which is exact
SELECT 0.1 + 0.2 = 0.3; -- t
-- Cast to binary floating point and equality fails
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8; -- f
-- SQLite: REAL is a double, and the printed value hides it
SELECT 0.1 + 0.2; -- 0.3
SELECT 0.1 + 0.2 = 0.3; -- 0 (false)
Beim SQLite-Paar sagt die Ausgabe 0.3 und der Vergleich etwas anderes. Beides stimmt.
Floats vergleichen: Warum == scheitert und Number.EPSILON keine Toleranz ist
0.1 + 0.2 === 0.3 ist false, weil die linke Seite ein anderes Double ist. Der übliche Rat lautet, stattdessen mit einer Toleranz zu vergleichen, und die am häufigsten wiederholte Fassung dieses Rats ist falsch:
Math.abs(a - b) < Number.EPSILON; // ⚠️ not a general-purpose comparison
Number.EPSILON ist 2.220446049250313e-16, also 2⁻⁵². MDN definiert die Konstante als die Differenz zwischen 1 und dem kleinsten Double größer als 1. Die Konstante beschreibt damit den Rasterabstand bei 1.0 und kein universelles Fehlerbudget. Der Abstand zwischen Floats verdoppelt sich bei jeder Zweierpotenz, ein fester Schwellenwert ist also überall falsch, außer in der Umgebung, in der er gemessen wurde.
In der Nähe von 1.0 geht es zufällig gut:
Math.abs((0.1 + 0.2) - 0.3); // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
Skalieren Sie dieselbe Rechnung um eine Milliarde nach oben, bricht sie zusammen:
const a = (0.1 + 0.2) * 1e9;
const b = 0.3 * 1e9;
a === b; // false
Math.abs(a - b); // 5.960464477539063e-8
Math.abs(a - b) < Number.EPSILON; // false — yet a and b are adjacent doubles
Rund um 1e9 beträgt der Abstand zwischen benachbarten Doubles etwa 1.19e-7. Da 1e9 in [2²⁹, 2³⁰) fällt, ist das ULP dort 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7, also etwa 500 Millionen Mal größer als Number.EPSILON. Jeder echte Rundungsfehler dieser Größenordnung überragt den Schwellenwert, die Prüfung schlägt also ausnahmslos fehl. Unten bei 1e-20 ist dieselbe Konstante viel zu großzügig und erklärt offensichtlich verschiedene Werte für gleich.
Absolute, relative und ULP-Toleranz
Für die Toleranz gibt es drei Ansätze, jeder für eine andere Situation:
- Die absolute Toleranz
|a − b| <= atolpasst, wenn Sie die Größenordnung vorab kennen. Sie ist außerdem das Einzige, was gegen null hin noch funktioniert, denn eine relative Toleranz um 0 herum ist selbst 0. - Die relative Toleranz
|a − b| <= rtol × max(|a|, |b|)skaliert mit den Daten über Größenordnungen hinweg, entartet aber nahe null. - Der ULP-Abstand zählt, wie viele darstellbare Werte zwischen den beiden Bitmustern liegen: am präzisesten, am schlechtesten lesbar.
Kombinieren Sie die ersten beiden, erhalten Sie die Form, die die gängigen Numerikbibliotheken verwenden:
function nearlyEqual(a, b, rtol = 1e-9, atol = 1e-12) {
if (a === b) return true; // handles Infinity === Infinity
const diff = Math.abs(a - b);
return diff <= Math.max(rtol * Math.max(Math.abs(a), Math.abs(b)), atol);
}
nearlyEqual(0.1 + 0.2, 0.3); // true
nearlyEqual((0.1 + 0.2) * 1e9, 0.3 * 1e9); // true
nearlyEqual(0, 1e-15); // true
nearlyEqual(1, 1.0001); // false
Python liefert das in der Standardbibliothek mit, und NumPys allclose benutzt dieselbe Formel:
import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12) # True
math.isclose(1e9, 1e9 + 1e-7, rel_tol=1e-9) # True
Für die strenge Variante bilden Sie die Bits auf eine monotone Ganzzahlordnung ab und subtrahieren:
const view = new DataView(new ArrayBuffer(8));
function toOrdinal(x) {
view.setFloat64(0, x);
const i = view.getBigInt64(0);
return i >= 0n ? i : -(1n << 63n) - i; // make negatives order correctly
}
function ulpDistance(a, b) {
const d = toOrdinal(a) - toOrdinal(b);
return d < 0n ? -d : d;
}
ulpDistance(0.1 + 0.2, 0.3); // 1n
ulpDistance((0.1 + 0.2) * 1e9, 0.3 * 1e9); // 1n
ulpDistance(-0, 0); // 0n
Exaktes == ist übrigens nicht immer falsch. Es taugt für ganzzahlige Doubles unter 2⁵³, für Konstanten, die Sie zugewiesen und nie weiterberechnet haben, und für die Prüfung, ob ein Wert exakt null ist.
Wenn Genauigkeit Geld kostet: den richtigen numerischen Typ wählen
Währungsbeträge und binäre Floats passen schlecht zusammen, und zwar nicht deshalb, weil ein einzelner Fehler groß wäre. Die Fehler sind systematisch und reproduzierbar, und sie fallen erst auf, wenn ein Prüfer nachrechnet:
19.99 * 100; // 1998.9999999999998
Math.round(19.99 * 100); // 1999
(1.005).toFixed(2); // '1.00' — 1.005 is stored as 1.00499999999999989...
Der zweite Fall erwischt Jahr für Jahr neue Teams. toFixed hat nicht falsch gerundet. Der übergebene Wert lag bereits unter 1.005.
Ganzzahlige Untereinheiten
Speichern Sie Cent, keine Euro. 1999 bedeutet 19.99 $, die Arithmetik läuft auf Ganzzahlen, und erst für die Anzeige rechnen Sie zurück.
const priceCents = 1999; // $19.99
const subtotal = priceCents * 3; // 5997 — exact
const totalCents = 1000; // $10.00 split three ways
const share = Math.floor(totalCents / 3); // 333
const remainder = totalCents - share * 3; // 1 cent to allocate
Zwei Einschränkungen. Oberhalb von Number.MAX_SAFE_INTEGER (9007199254740991, also 2⁵³ − 1) sind Ganzzahlen in JavaScript nicht mehr exakt, greifen Sie dort zu BigInt. Und Ganzzahlen entscheiden nicht über Ihre Rundungsrichtlinie: Division, Prozentsätze und Steueraufteilungen brauchen weiterhin eine explizite Regel dafür, wohin der übrig gebliebene Cent geht.
Dezimaltypen
Ein Dezimaltyp speichert Ziffern zur Basis 10, sodass Dezimalbrüche exakt landen. Erzeugen Sie ihn aus einer Zeichenkette, nie aus einem Float, sonst erben Sie den binären Fehler, bevor Sie überhaupt anfangen:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
Decimal(0.1) # 0.1000000000000000055511151231257827021181583404541015625
Console.WriteLine(0.1m + 0.2m); // 0.3
Console.WriteLine(0.1m + 0.2m == 0.3m); // True
Java hat BigDecimal, C# ein natives 128-Bit-decimal, SQL ein NUMERIC(12,2). Sie alle reparieren Dezimalbrüche. Keiner repariert 1/3, denn ein Dezimaltyp ist immer noch ein Gleitkommaformat; er hat die Basis nur von 2 auf 10 verschoben.
Entscheidungsmatrix
| Anwendungsfall | Empfohlener Typ | Warum |
|---|---|---|
| Geld, Abrechnung, Steuern | Ganzzahlige Untereinheiten oder Dezimaltyp | Exakte Dezimalarithmetik, prüfbare Rundung |
| Wissenschaft, Physik | double (FP64) | 15–16 Stellen reichen fast immer; beste Bibliotheksunterstützung |
| Geometrie, Grafik | float oder double + Toleranz | Ohnehin nur näherungsweise; mit Epsilon vergleichen |
| ML-Training | bfloat16 | Wertebereich von FP32 bei halbem Speicher |
| ML-Inferenz, Speicherung | FP16 | Mehr Mantissenbits, wenn die Werte normalisiert sind |
| Zähler, IDs, Schlüssel | 64-Bit-Ganzzahl oder BigInt | Hatte in einem Float nie etwas zu suchen |
Fehlerakkumulation und katastrophale Auslöschung
Ein Rundungsfehler ist 1e-17 groß und harmlos. Zehntausend davon sind ein Support-Ticket:
let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;
total; // 1000.0000000001588
total - 1000; // 1.588205122970976e-10
Die Drift wächst ungefähr mit der Anzahl der Operationen. In einer Schleife dieser Größe bleibt sie unsichtbar. In einer nächtlichen Aggregation über Millionen Zeilen steht sie am Morgen im Bericht.
Katastrophale Auslöschung
Der üblere Ausfall ist die Subtraktion. Ziehen Sie zwei fast gleiche Zahlen voneinander ab, löschen sich ihre übereinstimmenden führenden Ziffern aus, und übrig bleibt ein Ergebnis, in dem der bereits angesammelte Fehler dominiert. Der absolute Fehler wächst nicht. Der relative Fehler explodiert, weil das Ergebnis jetzt winzig ist, der Fehler aber gleich groß blieb.
Die Lehrbuchformel für die Varianz in einem Durchgang, E[x²] − (E[x])², läuft geradewegs hinein:
const data = [1e9, 1e9 + 1, 1e9 + 2];
const mean = data.reduce((s, x) => s + x, 0) / data.length;
// One-pass: E[x²] − (E[x])²
data.reduce((s, x) => s + x * x, 0) / data.length - mean * mean; // 0
// Two-pass: centre the data first
data.reduce((s, x) => s + (x - mean) ** 2, 0) / data.length; // 0.6666666666666666
Die wahre Populationsvarianz ist 2/3. Die Formel mit einem Durchgang liefert exakt null, eine Antwort, die mit 2/3 nichts mehr gemein hat: Beide Operanden lagen bei rund 1e18, und ihre Differenz fällt unter das letzte gespeicherte Bit. Welfords Online-Algorithmus umgeht das, indem er Mittelwert und Summe der quadrierten Abweichungen schrittweise fortschreibt.
Die Lösungsformel für quadratische Gleichungen, in deutschen Schulbüchern die Mitternachtsformel, hat dieselbe Schwäche, sobald b² ≫ 4ac gilt, und dieselbe Art von Abhilfe:
const a = 1, b = 1e8, c = 1;
const disc = Math.sqrt(b * b - 4 * a * c);
(-b + disc) / (2 * a); // -7.450580596923828e-9 — about 25% wrong
(2 * c) / (-b - disc); // -1e-8 — correct
Beide Zeilen berechnen dieselbe Wurzel. Die erste subtrahiert zwei fast gleiche Zahlen; die zweite formt die Algebra so um, dass sie das nie tun muss. Goldbergs What Every Computer Scientist Should Know About Floating-Point Arithmetic ist nach wie vor die maßgebliche Darstellung von Auslöschung und Fehlerschranken.
Kahan-Summation
Kahans kompensierte Summation führt Buch über die niederwertigen Bits, die jede Addition wegwirft, und speist sie in der nächsten Iteration wieder ein:
function kahanSum(values) {
let sum = 0;
let compensation = 0;
for (const value of values) {
const y = value - compensation;
const t = sum + y;
compensation = (t - sum) - y; // the bits that fell off
sum = t;
}
return sum;
}
kahanSum(new Array(10000).fill(0.1)); // 1000 — exactly
Python schenkt Ihnen das. math.fsum([0.1] * 10_000) liefert 1000.0, und seit CPython 3.12 nutzt auch das eingebaute sum() die Neumaier-Kompensation, sodass sum([0.1] * 10_000) exakt ist, während eine explizite +=-Schleife weiterhin auf 1000.0000000001588 driftet.
Greifen Sie bei langen Summen, Finanzaggregaten und numerischer Integration zur Kompensation. Sparen Sie sie sich bei einer Handvoll Werte oder wenn Sie ohnehin schon zu einem exakten Typ gewechselt sind.
Die Sonderwerte, die Annahmen brechen
IEEE 754 reserviert Bitmuster für Werte, die sich nicht an die Regeln halten, die Sie erwarten:
NaN === NaN; // false — mandated by the standard
Number.isNaN(NaN); // true
[NaN].indexOf(NaN); // -1 (uses ===)
[NaN].includes(NaN); // true (uses SameValueZero)
-0 === 0; // true
Object.is(-0, 0); // false
1 / -0; // -Infinity
1e308 * 10; // Infinity — overflow never throws
Infinity - Infinity; // NaN
Number.MIN_VALUE; // 5e-324 — the smallest subnormal double
NaN ist absichtlich ungleich zu allem, auch zu sich selbst, damit eine fehlgeschlagene Berechnung sich nie als gültiges Ergebnis ausgeben kann. Prüfen Sie darauf mit Number.isNaN() oder Pythons math.isnan().
Der Überlauf ist leiser und gefährlicher: Er liefert ±Infinity und rechnet weiter, pflanzt sich durch die nachgelagerten Schritte fort, bis jemandem ein Diagramm voller Lücken auffällt. Die negative Null ist ein eigenes Bitmuster, das unter === trotzdem +0 gleicht; sie entsteht beim Unterlauf eines negativen Werts oder aus -1 * 0, und nur die Division oder Object.is macht sie sichtbar.
Subnormale Zahlen füllen die Lücke zwischen null und der kleinsten normalen Zahl. Ist das Exponentenfeld komplett null, entfällt die implizite führende 1, und die Mantisse schrumpft allmählich gegen null. Das garantiert, dass die Differenz zweier ungleicher Floats nie exakt auf null rundet. Der Preis dafür sind sinkende Genauigkeit und, auf mancher Hardware, ein steiler Leistungseinbruch. Die Schaltflächen für Sonderwerte im IEEE 754 Gleitkommazahlen-Konverter laden ±0, ±Infinity, NaN und die kleinste subnormale Zahl, sodass Sie jedes Bitmuster direkt untersuchen können.
float vs. double vs. FP16 vs. bfloat16
Derselbe Standard, unterschiedliche Budgets für Wertebereich und Gleitkomma-Genauigkeit:
| Format | Bits gesamt | Exponent | Mantisse | Dezimalstellen ca. | Größter endlicher Wert | Typischer Einsatz |
|---|---|---|---|---|---|---|
| binary16 (FP16) | 16 | 5 | 10 | ~3.3 | 65504 | GPU-Inferenz, kompakte Speicherung |
| bfloat16 | 16 | 8 | 7 | ~2.4 | ~3.39 × 10³⁸ | ML-Training |
| binary32 (float) | 32 | 8 | 23 | ~7.2 | ~3.40 × 10³⁸ | Grafik, Sensoren, GPU |
| binary64 (double) | 64 | 11 | 52 | ~15.9 | ~1.80 × 10³⁰⁸ | Standard überall sonst |
Exponentenbits kaufen Wertebereich, Mantissenbits kaufen Genauigkeit. FP16 steckt sein Budget in die Genauigkeit und endet bei 65504, nah genug an realen Gradientenwerten, dass ein Überlauf nach Infinity zur alltäglichen Gefahr wird. bfloat16 tauscht in die andere Richtung, behält die acht Exponentenbits von FP32 und lebt mit sieben Mantissenbits, sodass Werte, die FP16 zum Überlaufen bringen würden, glatt durchgehen und das Training selten Loss Scaling braucht.
Nehmen Sie standardmäßig double. Wechseln Sie erst zu einem schmaleren Format, nachdem Sie einen Speicher- oder Bandbreitenengpass gemessen haben, und prüfen Sie die Kosten der Verschmälerung, indem Sie denselben Wert im Konverter in jedem Format eingeben.
Praktische Regeln für die Gleitkomma-Genauigkeit
- Verwenden Sie nie
==auf berechneten Floats. Nehmen Sie eine kombinierte absolute und relative Toleranz, zugeschnitten auf Ihre Daten. Number.EPSILONist kein Schwellenwert. Es beschreibt das Raster bei 1.0 und sonst nichts.- Halten Sie Geldbeträge in ganzzahligen Untereinheiten oder einem Dezimaltyp und bauen Sie Dezimalzahlen immer aus Zeichenketten.
- Lange Summen kompensieren Sie mit Kahan, Neumaier oder
math.fsum. - Formen Sie Formeln um, die fast gleiche Größen subtrahieren, statt die Toleranzen darum herum enger zu ziehen.
- Formatieren Sie für Menschen explizit, mit
toFixed, einem f-String oderprintf. Ein Standard-reprgehört nie in die Ausgabe für Nutzer. - Übergeben Sie Zahlen über Servicegrenzen hinweg als Zeichenketten oder Ganzzahlen. Ein JSON-Roundtrip durch ein Double ist lautlos und verlustbehaftet.
- Für Währungsspalten wählen Sie
NUMERICstattFLOAT. Das nach dem Start zu korrigieren bedeutet eine Migration und eine Abstimmung der Bestände. - Denken Sie in Bits, wenn ein Wert unmöglich aussieht. Dieselbe Gewohnheit steckt hinter unserem Leitfaden zu bitweisen Operationen, und sie macht aus Gleitkomma-Rätseln Arithmetik, die Sie nachrechnen können.
FAQ
Ist Gleitkomma-Arithmetik kaputt?
Gleitkomma-Arithmetik ist nicht kaputt, sie folgt IEEE 754 exakt. Der Standard stellt reelle Zahlen mit einer endlichen Zahl von Binärstellen dar, und Dezimalbrüche wie 0.1 haben keine exakte Binärform, also wird der nächstgelegene darstellbare Wert gespeichert. Kaputt ist nur die Erwartung, dass jede Dezimalzahl hineinpasst.
Warum gibt Python 0.1 als 0.1 aus, 0.1 + 0.2 aber als 0.30000000000000004?
Pythons repr verwendet die kürzeste Round-Trip-Formatierung: Es gibt die kürzeste Dezimalzeichenkette aus, die zurück auf dasselbe Double geparst wird. "0.1" identifiziert sein Double bereits eindeutig. Die Summe 0.1 + 0.2 ist ein anderes Double als jenes, auf das "0.3" abbildet, also muss der Formatierer alle siebzehn Stellen ausgeben, um eindeutig zu bleiben.
Warum sollte ich Number.EPSILON nicht zum Vergleich zweier Floats nehmen?
Number.EPSILON (etwa 2.22e-16) ist der Abstand zwischen 1 und dem nächsten Double, kein universelles Fehlerbudget. Der Abstand zwischen Floats verdoppelt sich bei jeder Zweierpotenz, sodass benachbarte Doubles nahe 1e9 etwa 1.19e-7 auseinanderliegen. Jeder echte Rundungsfehler dort übersteigt EPSILON, der Vergleich schlägt also immer fehl. Nehmen Sie eine relative oder kombinierte Toleranz.
Kann ich Gleitkommazahlen für Geld verwenden, wenn ich am Ende runde?
Am Ende zu runden rettet Gleitkomma-Geldbeträge nicht. Fehler summieren sich über Additionen, Multiplikationen und mehrstufige Aufteilungen, und der Zeitpunkt der Rundung verändert die Summe. 19.99 * 100 ergibt bereits 1998.9999999999998. Eine Prüfung braucht reproduzierbare exakte Arithmetik: ganzzahlige Cent, decimal, BigDecimal oder SQL NUMERIC.
Welche Dezimalzahlen lassen sich exakt in einem Double speichern?
Nur Werte, die sich als m / 2ⁿ mit ganzen Zahlen m und n innerhalb der Genauigkeit des Formats schreiben lassen. Das deckt 0.5, 0.25, 0.125, 0.75 und 2.5 ab, dazu jede ganze Zahl bis 2⁵³. Kürzen Sie den Bruch zuerst: Jeder verbleibende Faktor 5 im Nenner, wie bei 0.1, 0.2, 0.3 oder 0.7, bedeutet, dass der Wert nur angenähert wird.
Warum ist 0.1 + 0.2 === 0.3 false, aber 0.5 + 0.25 === 0.75 true?
Weil 0.5, 0.25 und 0.75 gerade 2⁻¹, 2⁻² und 2⁻¹ + 2⁻² sind, alle exakt darstellbar, und ihre Summe keine Rundung braucht. 0.1, 0.2 und 0.3 werden jeweils nur näherungsweise gespeichert, und die Rundung der Summe der ersten beiden landet ein ULP über dem Double, auf das 0.3 abgebildet wird.
Kommt das in jeder Programmiersprache vor?
Jede Sprache, die auf binärer IEEE-754-Gleitkommaarithmetik aufsetzt, zeigt dasselbe Verhalten: JavaScript, Python, Java, C, C++, Go, Rust, Swift und FLOAT in SQL. Unterschiedlich sind nur die voreingestellte Ausgabegenauigkeit und die Frage, ob ein exakter Dezimaltyp mitgeliefert wird, etwa decimal in C# oder Pythons Modul decimal.