Skip to content
Zurück zum Blog
Tutorials

Gleitkomma-Genauigkeit: Warum 0.1 + 0.2 ≠ 0.3 ist

Warum die Gleitkomma-Genauigkeit 0.1 + 0.2 verfälscht, wie IEEE 754 rundet und wie man Floats in JS, Python und SQL sicher vergleicht — mit Online-Konverter.

12 Min. Lesezeit

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:

  1. Binäre Gleitkommazahlen können nur Zahlen der Form m × 2ⁿ darstellen, also Summen von Zweierpotenzen.
  2. Dezimalbrüche wie 0.1, 0.2 und 0.3 haben diese Form nicht und werden beim Einlesen gerundet.
  3. Jede Rechenoperation rundet ihr Ergebnis erneut auf den nächstgelegenen darstellbaren Wert.
DezimalzahlExakt darstellbar?Warum
0.5Ja2⁻¹
0.25Ja2⁻²
0.75Ja2⁻¹ + 2⁻²
2.5Ja2 + 2⁻¹
100JaJede ganze Zahl unter 2⁵³
0.1Nein1/10, der Nenner enthält den Faktor 5
0.2Nein1/5, gleicher Faktor
0.3Nein3/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

SpracheStandard-Float-Typ0.1 + 0.2 erscheint alsOption für exakte Dezimalzahlen
JavaScriptnumber (nur Double)0.30000000000000004Nichts eingebaut; Integer-Cents oder eine Bibliothek
Pythonfloat (Double)0.30000000000000004decimal.Decimal, fractions.Fraction
Javadouble0.30000000000000004BigDecimal (aus einer Zeichenkette erzeugen)
C#double0.30000000000000004decimal, 128 Bit zur Basis 10
Gofloat640.30000000000000004 (über Variablen)math/big.Rat
Rustf640.30000000000000004Crate rust_decimal
SQLFLOAT / DOUBLE PRECISION0.30000000000000004NUMERIC / 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| <= atol passt, 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

AnwendungsfallEmpfohlener TypWarum
Geld, Abrechnung, SteuernGanzzahlige Untereinheiten oder DezimaltypExakte Dezimalarithmetik, prüfbare Rundung
Wissenschaft, Physikdouble (FP64)15–16 Stellen reichen fast immer; beste Bibliotheksunterstützung
Geometrie, Grafikfloat oder double + ToleranzOhnehin nur näherungsweise; mit Epsilon vergleichen
ML-Trainingbfloat16Wertebereich von FP32 bei halbem Speicher
ML-Inferenz, SpeicherungFP16Mehr Mantissenbits, wenn die Werte normalisiert sind
Zähler, IDs, Schlüssel64-Bit-Ganzzahl oder BigIntHatte 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:

FormatBits gesamtExponentMantisseDezimalstellen ca.Größter endlicher WertTypischer Einsatz
binary16 (FP16)16510~3.365504GPU-Inferenz, kompakte Speicherung
bfloat161687~2.4~3.39 × 10³⁸ML-Training
binary32 (float)32823~7.2~3.40 × 10³⁸Grafik, Sensoren, GPU
binary64 (double)641152~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.EPSILON ist 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 oder printf. Ein Standard-repr gehö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 NUMERIC statt FLOAT. 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.

Tags: floating-point ieee-754 javascript python numbers

Verwandte Artikel

Alle Artikel anzeigen