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:
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
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:
| Kategorie | Vorzeichenbit | Exponent (Speicherung) | Mantisse | Bedeutung |
|---|---|---|---|---|
| Null | 0 oder 1 | 0 (alle Nullen) | Alle Nullen | +0 oder -0 |
| Subnormale Zahl | 0 oder 1 | 0 (alle Nullen) | Nicht alle Nullen | Werte sehr nahe bei 0 |
| Normalisierte Zahl | 0 oder 1 | 1 ~ 254 | Any | Normale Gleitkommazahlen |
| Unendlich | 0 oder 1 | 255 (alle Einsen) | Alle Nullen | +Unendlich oder -Unendlich |
| NaN | Any | 255 (alle Einsen) | Nicht alle Nullen | Nicht-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
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
| Szenarien | Empfohlener Typ | Grund |
|---|---|---|
| Wissenschaftliches Rechnen, Grafik-Rendering | float oder double | Ausreichende Genauigkeit, gute Unterstützung für Hardwarebeschleunigung |
| Berechnung von Finanzbeträgen | Decimal / BigDecimal | Exakte Dezimalarithmetik, keine Rundungsfehler |
| Zähler, Indizes | int / long | Exakte Ganzzahlen, keine Präzisionsprobleme |
| Gleitkommazahlen vergleichen | Mit Toleranz vergleichen | Verwende nicht direkt ==; verwende abs(a-b) < epsilon |
| Währungsspeicherung/-übertragung | Ganzzahlen (in Cent) | Gleitkommafehler bei der Serialisierung vermeiden |