Zum Inhalt springen
Topos Uranos

Zusammenfassung

Die Zahlen mit Nachkommastellen eines Programms sind nicht die reellen Zahlen der Mathematik: Sie bilden eine endliche Teilmenge der rationalen Zahlen, deren Nenner eine Zweierpotenz ist, und jeder Wert, der nicht zu dieser Teilmenge gehört, wird durch den nächstgelegenen ersetzt, der dazugehört. Diese Lektion untersucht, wie eine Gleitkommazahl in 64 Bits gespeichert wird (Vorzeichen, Exponent und Mantisse), warum 0.10.1 keine exakte Darstellung hat und welcher Wert an seine Stelle tritt, wie viele Ziffern in float und in double sicher sind, wie sich Rundungsfehler anhäufen, warum man zwei reelle Zahlen nicht mit == vergleicht und wie man sie mit einer begründeten Toleranz vergleicht, und was Unendlich und der Nicht-Zahl-Wert sind, die das Format für Ergebnisse reserviert, die nicht hineinpassen oder nicht existieren.

Lernziele

  1. Erklären, warum 0.10.1 keine exakte Binärdarstellung hat und welcher Wert stattdessen gespeichert wird.
  2. Die Wirkung der Rundung bei wiederholten Additionen und bei der Summe von Zahlen sehr verschiedener Größenordnung vorhersagen.
  3. Reelle Zahlen mit einer geeigneten Toleranz statt mit == vergleichen und die Wahl der Toleranz begründen.
  4. Je nach geforderter Genauigkeit zwischen float und double wählen und Unendlich sowie den Nicht-Zahl-Wert erkennen.

Wissenschaftliche Schreibweise zur Basis 2

Die Lektion Ganze Zahlen und ihre Darstellung hat gezeigt, dass eine ganze Zahl mit nn Bits genau 2n2^n aufeinanderfolgende Werte darstellt. Nun stellen Zahlen mit Nachkommastellen eine andere Schwierigkeit: Zwischen zwei beliebigen reellen Zahlen liegen unendlich viele andere, und keine endliche Anzahl von Bits kann sie alle unterscheiden. Der Ausweg, den alle heutigen Computer gewählt haben, ist derselbe, mit dem die Wissenschaft sehr große oder sehr kleine Größen schreibt: die wissenschaftliche Schreibweise. Zur Basis 10 schreibt man die mittlere Entfernung der Erde von der Sonne als 1.496×10111.496 \times 10^{11} Meter; die Zahl hat ein Vorzeichen, einige wenige signifikante Ziffern (die Mantisse, 1.4961.496) und eine Potenz der Basis (den Exponenten, 1111), die angibt, wo das Komma steht. Wenn dem so ist, deckt man mit einer festen Anzahl von Ziffern Größen sehr verschiedener Ordnungen ab, um den Preis, von jeder nur die ersten Ziffern zu speichern.

Der Computer tut dasselbe zur Basis 2. Jede von null verschiedene Zahl lässt sich auf genau eine Weise schreiben als

x=(−1)s×1.m1m2m3…m52 (2)×2ex = (-1)^{s} \times 1.m_1 m_2 m_3 \ldots m_{52} \, {}_{(2)} \times 2^{e}

wobei ss das Vorzeichen ist (0 für positiv, 1 für negativ), 1.m1m2…1.m_1 m_2 \ldots die binär geschriebene Mantisse, deren erste Ziffer immer 1 ist, und ee der Exponent. Dass die erste Ziffer immer 1 ist, hat eine praktische Folge: Man muss sie nicht speichern, und das eingesparte Bit kommt der Genauigkeit zugute. Der Name Gleitkomma beschreibt genau diese Form: Das Komma hat keinen festen Platz zwischen den Ziffern, sondern „gleitet“ je nach Exponent.

Der Typ double von C++ verwendet in allen aktuellen Compilern das international genormte binäre Format doppelter Genauigkeit mit 64 Bits: 1 Bit für das Vorzeichen, 11 Bits für den Exponenten und 52 Bits für die Mantisse, ohne die führende Ziffer. Der Exponent wird verschoben gespeichert: Das 11-Bit-Feld enthält e+1023e + 1023, sodass es vorzeichenlose Werte von 1 bis 2046 für Exponenten von −1022-1022 bis 10231023 speichert (die Werte 0 und 2047 des Feldes sind für die Null, die sehr kleinen Zahlen, Unendlich und den Nicht-Zahl-Wert reserviert, die am Ende der Lektion behandelt werden). Der Typ float verwendet das Format einfacher Genauigkeit mit 32 Bits: 1 für das Vorzeichen, 8 für den Exponenten und 23 für die Mantisse.

Die Bits eines double lassen sich ansehen. Das folgende Programm verwendet std::bit_cast aus dem Header <bit>, das die 64 Bits der Zahl als vorzeichenlose 64-Bit-Ganzzahl neu interpretiert, ohne eines davon zu ändern; hier wird es ohne weitere Erklärung verwendet, als Instrument, um in den Speicher zu schauen. Die Formatangabe {:064b} schreibt diese Ganzzahl binär mit 64 Ziffern, wie in der vorigen Lektion, und {:016X} hexadezimal mit 16.

C++-Programm
#include <bit>#include <cstdint>#include <print>​int main(){    const double tenth{0.1};    // Die 64 Bits der Zahl, als vorzeichenlose Ganzzahl gelesen.    const std::uint64_t bits{std::bit_cast<std::uint64_t>(tenth)};    std::println("{:064b}", bits);    std::println("{:016X}", bits);}
Ausgabe
00111111101110011001100110011001100110011001100110011001100110103FB999999999999A
BeispielDie 64 Bits von 0.1

Die Abbildung gruppiert die 64 Bits, die das Programm geschrieben hat, und liest sie Feld für Feld.

Beweis

  1. 1+11+52=641 + 11 + 52 = 64

    Die 64 Bits, hexadezimal 3FB999999999999A. Sie werden von links nach rechts gelesen: 1 Vorzeichenbit, 11 Exponentenbits und 52 Mantissenbits.

  2. s=0s = 0

    Das Vorzeichen ist 0: Die Zahl ist positiv.

  3. e=1019−1023=−4e = 1019 - 1023 = -4

    Das Feld des Exponenten, binär 0111111101101111111011, hat den Wert 1019; davon wird die Verschiebung, 1023, abgezogen, und der Exponent ist −4-4.

  4. m≈1.6000000000000000888m \approx 1.6000000000000000888

    Die Mantisse, mit der führenden 1, die nicht gespeichert wird, ist binär 1.1001100110011…10101.1001100110011\ldots1010: Die Gruppe 10011001 wiederholt sich, und die letzte Gruppe, 10101010, ist aufgerundet. Ihr Wert ist ungefähr 1.60000000000000008881.6000000000000000888.

  5. x=0.1000000000000000055511…≠0.1x = 0.1000000000000000055511\ldots \neq 0.1

    Die gespeicherte Zahl ist +1.6000000000000000888×2−4+1.6000000000000000888 \times 2^{-4}, also 0.1000000000000000055511…0.1000000000000000055511\ldots: sehr nahe bei 0.10.1, aber nicht 0.10.1.