Wie werden Dezimalzahlen dargestellt? -- Gleitkommazahlen und IEEE 754

In dieser Lektion werden Sie verstehen: wie Dezimalzahlen in einer Welt dargestellt werden, die nur aus Nullen und Einsen besteht, und warum Gleitkommazahlen Präzisionsfehler haben.


Alltagsvergleich: wissenschaftliche Notation

Sie haben sicher schon wissenschaftliche Notation im Physikunterricht gesehen:

Lichtgeschwindigkeit = 3.0 x 108m/s, Elektronenmasse = 9.1 x 10-31 kg.

Mit dieser Methode lassen sich sehr große oder sehr kleine Zahlen mit sehr wenigen Ziffern darstellen.

Computer verwenden genau dieselbe Idee, um Dezimalzahlen darzustellen--Binäre wissenschaftliche Notation。

Jede binäre Zahl lässt sich schreiben als:

1.xxxxxxxxxx x 2yyyyy

Diese Form muss nur drei Dinge speichern:

  • Vorzeichen: positiv oder negativ?
  • Mantisse(xxxxxxxxxx): Teil der signifikanten Stellen
  • Exponent(yyyyy): Position des Dezimalpunkts

Der IEEE-754-Standard packt diese drei Dinge in 32 Bits in einem festen Format.


Die Drei-Segment-Struktur von IEEE 754 mit einfacher Genauigkeit

32 Bits (Gleitkommazahl mit einfacher Genauigkeit) sind genau in drei Segmente unterteilt:

Vorzeichenbit 0 1 Bit
Exponent 00000000 8 Bits
Mantissenbits Mantisse (Fraktion) 00000000000000000000000 23 Bits
Vorzeichenbit (1 Bit)
Exponentenbits (8 Bits)
Mantissenbits (23 Bits)

Erster Teil: Vorzeichenbit (1 Bit)

Das einfachste Segment.

0repräsentiert positive Zahlen,1repräsentiert negative Zahlen.

Zweites Segment: Exponentenbits (8 Bits)

Der Exponent bestimmt die Position des Dezimalpunkts (daher stammt die Bezeichnung "Gleitkomma" -- der Dezimalpunkt kann "schweben").

Aber der Exponent wird nicht direkt gespeichert; stattdessen wird "tatsächlicher Exponent + 127" gespeichert.

Diese 127 wirdOffset (Bias) genannt。

Warum einen Offset addieren?

Weil 8 Bits nur 0~255 (ohne Vorzeichen) darstellen können, der tatsächliche Exponent aber negativ sein muss (um Zahlen kleiner als 1 darzustellen).

Nach dem Hinzufügen des Offsets 127:

  • Der gespeicherte Wert 127 entspricht dem tatsächlichen Exponenten 0 (127 - 127 = 0)
  • Der Speicherwert 0 entspricht dem tatsächlichen Exponenten -127
  • Der Speicherwert 255 entspricht dem tatsächlichen Exponenten +128

Drittes Segment: Mantissenbits (23 Bits)

Die Mantisse speichert den Teil "xxxxx" von "1.xxxxx" und lässt dabei die führende 1 weg.

Weil in der binären wissenschaftlichen Notation die normalisierte Form immer 1.xxx ist, muss die führende 1 nicht gespeichert werden.

Dies spart auf clevere Weise ein zusätzliches 1 Bit an Präzision.

Der Schlüssel zum Verständnis der Drei-Segment-Struktur: Gleitkommazahl = (-1)^Symbol× 1.Mantisse × 2(Exponent-127)Das ist die gesamte Essenz von IEEE 754.


Interaktive Demo: 32-Bit-Gleitkommazahl-Zerleger

Geben Sie eine Gleitkommazahl ein und beobachten Sie ihre Drei-Segment-Struktur im IEEE-754-Einfachpräzisionsformat.

Der segmentierte Balken hebt hervor, auf welche Bits sich jedes der drei Segmente bezieht.

IEEE-754-Dissektor für einfache Genauigkeit

Gleitkommazahl eingeben:

Versuchen Sie, 0.1 einzugeben, und beobachten Sie den Mantissenteil.

Die binäre Mantisse von 0.1 ist eine unendlich periodische Dezimalzahl--23 Bits sind völlig unzureichend, um sie zu speichern.

Dies führt zu einem berühmten Phänomen in der Informatik.


Warum 0.1 + 0.2 nicht gleich 0.3 ist

Wenn Sie dies in Python oder JavaScript ausführen0.1 + 0.2 == 0.3, lautet das ErgebnisFalse。

Dies ist kein Fehler, sondern eine unvermeidliche Folge der Gleitkommadarstellung.

Der Grund ist ganz einfach:0.1 kann binär nicht exakt dargestellt werden。

Genau wie im Dezimalsystem ist 1/3 = 0.33333... eine unendlich periodische Dezimalzahl.

Im Binärsystem ist 0.1 (dezimal) = 0.000110011001100110011... (binär), was ebenfalls ein unendlich periodischer Bruch ist.

IEEE 754 hat nur 23 Bits Mantisse, um diese unendlich periodische Dezimalzahl zu speichern.

Der über 23 Bits hinausgehende Teil kann nur abgeschnitten (gerundet) werden, was einen Rundungsfehler erzeugt.

Der tatsächlich für 0.1 in float32 gespeicherte Wert ist ungefähr 0.100000001490116119384765625.

Der tatsächlich für 0.2 in float32 gespeicherte Wert ist ungefähr 0.20000000298023223876953125.

Nach der Addition der beiden und dem Abschneiden ist das Ergebnis nicht genau 0.3.

Betragsberechnungen müssen Gleitkommazahlen vermeiden

In Szenarien mit Geld sollten Sie Ganzzahlen (in Einheiten von "Cents") oder einen dedizierten Decimal-Typ verwenden.

PythonsDecimalJavasBigDecimalC#'sdecimalsind alle dafür ausgelegt.

Wenn Sie Geld mit Gleitkommazahlen speichern, wurde das Problem bereits vor langer Zeit gesät, wenn Sie feststellen, dass der akkumulierte Fehler von 0.01 zu 0.010000000000000002 geworden ist.


Wertebereich von Gleitkommazahlen und speziellen Werten

IEEE 754 reserviert mehrere spezielle Kombinationen für den Exponenten und die Mantisse:

KategorieVorzeichenbitExponent (Speicherung)MantisseBedeutung
Null0 oder 10 (alle Nullen)Alle Nullen+0 oder -0
Subnormale Zahl0 oder 10 (alle Nullen)Nicht alle NullenWerte sehr nahe bei 0
Normalisierte Zahl0 oder 11 ~ 254AnyNormale Gleitkommazahlen
Unendlich0 oder 1255 (alle Einsen)Alle Nullen+Unendlich oder -Unendlich
NaNAny255 (alle Einsen)Nicht alle NullenNicht-numerische Werte (z. B. 0/0)

Ungefährer Bereich von Gleitkommazahlen einfacher Genauigkeit (32 Bit):

  • Maximaler positiver Wert: ungefähr 3.4 x 1038
  • Kleinster positiver Wert (normalisiert): ungefähr 1.2 x 10-38
  • Kleinster positiver Wert (denormalisiert): ungefähr 1.4 x 10-45
  • Effektive Genauigkeit: ungefähr 7 signifikante Dezimalstellen

Double mit doppelter Genauigkeit (64 Bit) hat einen größeren Bereich und eine höhere Genauigkeit:

  • 11-Bit-Exponent (Bias 1023)
  • 52-Bit-Mantisse
  • Effektive Präzision: ungefähr 15~16 signifikante Dezimalstellen

Drei Fälle von Präzisionsverlust bei Gleitkommazahlen

Fall 1: Fehler bei der Dezimal-zu-Binär-Umrechnung

Wie das frühere 0.1-Beispiel.

Jede Dezimalzahl, die im Dezimalsystem "sauber" ist, kann im Binärsystem unendlich periodisch sein.

Nur Brüche, deren Nenner eine Zweierpotenz ist (wie 0.5, 0.25, 0.125), können im Binärsystem exakt dargestellt werden.

Fall 2: Addition einer kleinen Zahl zu einer großen Zahl

Die Präzision von Gleitkommazahlen ist nicht gleichmäßig verteilt – je größer der Wert, desto größer die kleinste unterscheidbare Differenz.

Zum Beispiel 16777216.0 + 1.0 = 16777216.0 (in float32 bleibt das Ergebnis unverändert).

Weil eine 32-Bit-Gleitkommazahl nur etwa 7 signifikante Stellen exakt darstellen kann und 16777216 selbst bereits 8 Stellen belegt.

Die durch das Addieren von 1 erzeugte Änderung fällt in den Genauigkeitsbereich, den die Mantisse nicht darstellen kann, und wird direkt "verschluckt".

Fall 3: Subtraktion zweier nahezu gleicher Zahlen

Die Subtraktion zweier sehr dicht beieinanderliegender Gleitkommazahlen kann zueinem katastrophalen Verlust signifikanter Stellen führen.。

Zum Beispiel: 1.2345678 - 1.2345677 = 0.0000001, aber die tatsächliche Berechnung kann 0.000000099999... ergeben, ein Ergebnis mit einem großen Fehler.

Die Genauigkeitsgrenzen von Gleitkommazahlen zu kennen, soll nicht dazu führen, dass Sie "Angst" davor haben, sie zu verwenden, sondern Ihnen helfen, das richtige Werkzeug für das richtige Szenario zu wählen. Wissenschaftliches Rechnen, Grafikrendering, maschinelles Lernen -- Gleitkommazahlen sind in diesen Bereichen völlig ausreichend. Aber Finanzen, Buchhaltung, präzises Zählen -- halten Sie sich von Gleitkommazahlen fern.


Code-Demonstration: Die Geheimnisse der Gleitkommazahlen

Beispiel

# Vollständige Demo des Gleitkomma-Genauigkeitsproblems (Beispiel-Demo)
import struct

def float_to_ieee754_binary(num):
    Eine Gleitkommazahl in eine IEEE-754-Dreisegmentdarstellung mit einfacher Genauigkeit umwandeln
    # Verwende struct, um die Gleitkommazahl in 4 Bytes zu packen (Big-Endian)
    packed = struct.pack('>f', num)
    # In einen 32-Bit-Binärstring umwandeln
    bits = ''.join(f'{b:08b}' for b in packed)

    sign = bits[0]
    exponent = bits[1:9]
    mantissa = bits[9:32]

    exp_stored = int(exponent, 2)
    exp_actual = exp_stored - 127

    # Berechne den Dezimalwert der Mantisse
    frac = 0
    for i, bit in enumerate(mantissa):
        if bit == '1':
            frac += 2 ** -(i + 1)

    value = (-1 if sign == '1' else 1) * (1 + frac) * (2 ** exp_actual)

    return {
        'bits': bits,
        'sign': sign,
        'exponent': exponent,
        'mantissa': mantissa,
        'exp_stored': exp_stored,
        'exp_actual': exp_actual,
        'fraction': frac,
        'reconstructed': value
    }

# =============================================
# Demo 1: Vergleich der Darstellung von 0.5 und 0.1
# =============================================
print("=" * 60)
print("Demo 1: Warum 0.5 exakt ist, aber 0.1 nicht")
print("=" * 60)

for num in [0.5, 0.1]:
    r = float_to_ieee754_binary(num)
    print(f"\nGleitkommazahl: {num}")
    print(fVorzeichenbit: {r['sign']} ({'positiv' if r['sign'] == '0' else 'negativ'}))
    print(f" Exponentenbits: {r['exponent']} (Speicher={r['exp_stored']}, tatsächlich={r['exp_actual']})")
    print(fMantissenbits: {r['mantissa']})
    print(fTatsächlich gespeicherter Wert: {r['reconstructed']})

# =============================================
# Demo 2: Der Klassiker 0.1 + 0.2 != 0.3
# =============================================
print("\n" + "=" * 60)
print(Demo 2: 0.1 + 0.2 != 0.3)
print("=" * 60)

# Mit höherer Genauigkeit anzeigen
import decimal
ctx = decimal.getcontext()
ctx.prec = 50

a = decimal.Decimal(0.1)
b = decimal.Decimal(0.2)
c = decimal.Decimal(0.3)

print(f"Tatsächlicher Wert von 0.1: {a}")
print(f"Tatsächlicher Wert von 0.2: {b}")
print(f"Tatsächlicher Wert von 0.3: {c}")
print(f"0.1 + 0.2  = {a + b}")
print(f"0.1 + 0.2 == 0.3 ? {0.1 + 0.2 == 0.3}")

# =============================================
# Demo 3: Große Zahlen 'verschlucken' kleine Zahlen
# =============================================
print("\n" + "=" * 60)
print("Demo 3: Große Zahl plus kleine Zahl wird verschluckt")
print("=" * 60)

groß = 16777216.0
gering = 1.0
result = big + small
print(f"{big} + {small} = {result}")
print(f"Entspricht das Ergebnis der ursprünglichen Zahl? {result == big}")

# Zeigt, dass diese Zahl die Präzisionsgrenze von float32 erreicht
print(f"\nGrund: {big} In float32 Medium erfordert {len(format(struct.unpack('>I', struct.pack('>f', big))[0], '032b')[:23])} Bits Mantisse")
print(fDer minimale Abstand zwischen zwei benachbarten float32-Werten beträgt ungefähr 2^24 / 2^23 = 2.0)

# =============================================
# Demo 4: Korrekte Techniken zur Umwandlung der Groß-/Kleinschreibung
# =============================================
print("\n" + "=" * 60)
print("Demo 4: EXAMPLE example - Korrekte Handhabung von Beträgen")
print("=" * 60)

from decimal import Decimal

# Falscher Ansatz: Verwendung von Gleitkommazahlen
float_total = 0.1 + 0.2
print(fFehler (Gleitkomma): 0.1 + 0.2 = {float_total})

# Korrekter Ansatz: Decimal verwenden
decimal_total = Decimal('0.1') + Decimal('0.2')
print(f"Korrekt bestätigt(Decimal): Decimal('0.1') + Decimal('0.2') = {decimal_total}")

# Oder ganze Zahlen verwenden (in Cent)
cents_total = 10 + 20  # 0.1 yuan = 10 fen, 0.2 yuan = 20 fen
print(f"Korrekte Bestätigung(Ganzzahldivision): 10 fen + 20 fen = {cents_total} fen = {cents_total/100}元")

# =============================================
# Demo 5: Spezielle Werte von Gleitkommazahlen
# =============================================
print("\n" + "=" * 60)
print("Demo 5: Spezielle Werte von Gleitkommazahlen")
print("=" * 60)

import math

# Unendlich
print(f"float('inf') = {float('inf')}")
print(f"1.0 / 0.0       = {1.0 / 0.0}")
print(f"float('inf') > 1e1000000 = {float('inf') > 1e1000000}")

# NaN
print(f"float('nan') = {float('nan')}")
print(f"0.0 / 0.0        = {0.0 / 0.0}")
print(f"float('nan') == float('nan') ? {float('nan') == float('nan')}")
print(f" (NaN ist mit keinem Wert gleich, auch nicht mit sich selbst!)")

# Auf NaN prüfen
print(f"math.isnan(float('nan')) = {math.isnan(float('nan'))}")

Praktische Ratschläge: Wann verwendet man was

SzenarienEmpfohlener TypGrund
Wissenschaftliches Rechnen, Grafik-Renderingfloat oder doubleAusreichende Genauigkeit, gute Unterstützung für Hardwarebeschleunigung
Berechnung von FinanzbeträgenDecimal / BigDecimalExakte Dezimalarithmetik, keine Rundungsfehler
Zähler, Indizesint / longExakte Ganzzahlen, keine Präzisionsprobleme
Gleitkommazahlen vergleichenMit Toleranz vergleichenVerwende nicht direkt ==; verwende abs(a-b) < epsilon
Währungsspeicherung/-übertragungGanzzahlen (in Cent)Gleitkommafehler bei der Serialisierung vermeiden
Weitere Erweiterungen