Das Floating-Point-Format im Detail
Teil 1 von 3 / 1. März 2004 / von aths / Seite 5 von 13
Fließkomma im Binärformat
Wenden wir uns der binären Darstellung von Zahlen zu. Auf das FX12-Format gingen wir schon ein: Das erste Bit repräsentiert das Vorzeichen, das zweite Bit den Vorkomma-Anteil, die restlichen 10 Bit stehen für den Nachkomma-Anteil. Es wäre zu überlegen, neben dem Nachkomma-Anteil zusätzlich zu speichern, wo das Komma eigentlich stehen soll, um auf Wunsch mehr Bits für den Vorkomma-Bereich zu haben. Doch bringen wir etwas Systematik in die Überlegungen!
Die "Ausgangsposition" legen wir mal so fest, dass wir binär "1" darstellen, mit dem Komma also binär 1,0. Wird das Komma nach rechts gerückt, erhalten wir binär 10,0 - dezimal also 2. Wird das Komma nach links gerückt, gelangen wir zu binär 0,1, dezimal 0,5. Man sieht leicht, dass sich die Werte pro Verschiebung der Komma-Stelle je nach "Schieberichtung" verdoppeln oder halbieren. Das heißt, dass mit der Komma-Position ein Bereich festgelegt werden kann (man denke an das Euro-Beispiel).
Wir brauchen noch eine Möglichkeit, um in jedem dieser Bereiche abzustufen. Zum Beispiel mit einem Faktor von 1 bis knapp 2. Kleines Gedankenspiel: Ohne das Komma zu verschieben, haben wir den "Grund-Bereich" ab 1 und könnten mit einem Faktor, der von 1 bis knapp 2 reicht, diesen Grundbereich bis zum nächsten ausfüllen, der ja wie gezeigt mit 2 beginnt. Verschieben wir das Komma um eine Stelle nach rechts, so dass wir im Grundbereich 2 sind, stufen wir mit einem Faktor von 1 bis knapp 2 die Zahlenwerte von 2*1 = 2 bis 2*1,999.. = knapp 4 ab. Bei 4 würde auch der nächste Grundbereich beginnen. Das Prinzip funktioniert auch, wenn das Komma nach links geschoben wird: Beim Grundbereich 0,5 stufen wir mit einem Faktor-Bereich von 1 bis knapp 2 so ab, dass wir Zahlen von 0,5 bis knapp 1 darstellen können.
Man speichert einmal die Position des Kommas und legt damit den Bereich fest, in dem der Faktor gilt. Weil jedes Verrücken des Kommas um eine Stelle die Wertigkeit der Zahl verdoppelt bzw. halbiert, muss der Faktor Zahlen im Bereich von 1 bis knapp 2 darstellen zu können. Kombiniert man beides, lässt sich praktisch jede beliebige Zahl darstellen. Nehmen wir als Beispiel die dezimale 12. Binär entspricht 12 = 1100,0. Wir könnten das genauso gut so speichern:1,1000 + Verschiebung des Kommas um 3 Stellen nach rechts. 1,1 ist nun dezimal 1,5. Da jede Komma-Verschiebung nach rechts eine Verdoppelung des Faktors bedeutet, hätten wir mit einer Verschiebung um zwei Stellen die Wertigkeit vervierfacht, und bei drei Stellen verachtfacht. In der Tat: 1,5 * 8 = 12.
Der Faktor wird in den richtigen Bereich übertragen.
Alternativ: Der Faktor verdoppelt sich bei dieser "Komma-Schiebe-Richtung" pro Verschiebung.
Wir schieben das binäre Komma um drei Positionen nach rechts. Damit wird der Zahlenwert jeweils verdoppelt.
Das kann man auch eleganter formulieren: Verschieben wir das Komma gar nicht, entspricht das einer Multiplikation mit 2^0 = 1. Verschieben wir das Komma um eine Position nach rechts, fängt der Bereich bei 2^1 = 2 an. Zwei Verschiebungen = 2^2 = 4 und drei Verschiebungen = 2^3 = 8. Die Komma-Position ist also nichts anderes als ein Exponent zur Basis 2. Wir rechnen ja 2^(Komma-Verschiebung), gesprochen "Zwei hoch Komma-Verschiebung" - und das, was bei "hoch" steht, hat nunmal den Fachausdruck "Exponent".
Um vom FP-Format zurückzurechnen, nutzt man die Formel (2^Exponent) * Faktor. 2^3 * 1,5 = 8 * 1,5 = 12. Ist das nicht irre aufwändig, multiplizieren zu müssen, um die gespeicherte Zahl zu ermitteln? Nun, für den Rechner stellen diese Multiplikationen kein Problem dar - denn sie alle können durch eine Verschiebung des Kommas ausgeführt werden. Das geht sehr viel schneller, als "richtig" zu multiplizieren.
In der Tat optimieren CPUs längst so, dass Multiplikationen möglichst durch Schiebe-Operationen ersetzt werden. Zu Zeiten, als noch normale VGA-Karten mit 256 kB RAM modern waren, nutzte man für Spiele oder Grafik-Demos öfters den MCGA-Modus, welcher eine Auflösung von 320x200 mit 256 Farben bot. Der Bildschirm-Speicher wird dabei linear adressiert. Die Speicheradresse eines Pixels ist dann X + (Y * 320). Auf den damaligen CPUs kostete ein Integer-MUL viele Takte. Damals optimierte man noch von Hand: Das MUL 320 wurde aufgespalten in zwei Shift-Operationen und ein ADD. Denn Y * 320 entspricht Y * 64 + Y * 256. 64 und 256 sind Zweierpotenzen und lassen sich durch schnelle Shift-Operationen erledigen. Das in Handarbeit zu machen, ist heute nicht mehr nötig - Compiler und CPU optimieren automatisch (man kann zum Beispiel den Karatsuba-Algorithmus dafür verwenden).
Wichtig ist eins: Um eine binär gespeicherte Floating-Point-Zahl in unser Dezimal-System zu überführen, sind Multiplikationen grundsätzlich notwendig. Computer arbeiten aber ohnehin im Binär-Format, sie können mit reinen Bitschiebe-Operationen arbeiten, um die Zahl zu "verstehen".
Das Floating-Point-Format im Detail
Wenn wir zur Umrechnung einer Binärzahl in das Fließkomma-Format die führende 1 suchen, und uns dahinter das Komma denken, kann es sein, dass nur noch Nullen folgen. Also erhalten wir mindestens 1,0 als Faktor. Falls ausschließlich Einsen folgen, bekommen wir binär 1,1111..., also garantiert eine Zahl kleiner als 2. (die 2,0 ist im Binärformat 10,0; da wir das Komma direkt hinter die erste Eins setzen, die wir finden, ist es unmöglich, als Faktor eine binäre 10,0 zu bekommen). Das passt natürlich sehr gut zusammen, da wir festgestellt haben, dass der Faktor exakt diese Spannweite von 1 bis knapp 2 haben muss.
Reservieren wir zehn Bit für den Faktor, sähe das im Binär-Format sähe so aus: 1,0'0000'0000 für die 1, und 1,1'1111'1111 für die "knapp" 2 (dezimal 1,998). Jeder unserer Faktoren beginnt mit "1,". Das bräuchte man ja eigentlich nicht extra zu speichern! Genau so wird auch verfahren: Man speichert nur den Nachkomma-Anteil, auch Mantisse genannt (das kommt aus dem lateinischen, englisch heißt das "mantissa"). Wir sparen die führende "1," und haben dadurch ein Bit mehr für den Nachkomma-Anteil, und lösen ihn somit noch mal doppelt so fein auf. Der Faktor 1 entspricht also (1),00'0000'0000, und "fast zwei" (1),11'1111'1111, umgerechnet 1,999. Da wir festlegen, dass zur Mantisse noch eine führende "1," gehört, spricht man auch von einer normalisierten Mantisse. Weil die "1," nicht mitgespeichert wird, ist es außerdem eine packed mantissa.
Außerdem brauchen wir für die Darstellung negativer Zahlen ein Vorzeichen-Bit, kurz Sign. Einen Exponenten brauchen wir natürlich auch. Wollen wir FP16 mit einer 10-Bit-Mantisse darstellen, bleiben abzüglich des Signs noch 5 Bit für den Exponenten. Wir haben die "physischen" Exponenten von 00000 bis 11111, also von 0 bis 31. Für die Darstellung von Zahlen kleiner 1 brauchen wir jedoch auch negative Exponenten. Deshalb wird ein Bias definiert. Dieser wird nicht gespeichert, sondern muss der Hardware bekannt sein. Bei FP16 ist das Bias -15. Aus den physischen Exponenten von 0 bis 31 werden somit logische Exponenten von -15 bis 16.
Damit haben wir nun alles zusammen: seee'eemm'mmmm'mmmm: 1 Bit Vorzeichen, 5 Bit Exponent, 10 Bit Mantisse. Wir kennen jetzt die Bestandteile von Floating-Point-Zahlen, und ihre Bedeutung. Übrigens wird die FP16-Definition auch so angegeben: s10e5. Das heißt Sign, 10 Bit Mantisse, 5 Bit Exponent. Obwohl die interne Darstellung erst den Exponenten und dann die Mantisse speichert, gibt man bei der Beschreibung neben dem Vorzeichen-Bit erst die Mantisse und dann den Exponenten an.
Was gewinnt man nun durch diese doch recht komplexe Darstellung? Das betrachten wir im nächsten Teil. Dort gehen wir auch auf Besonderheiten ein, die es nur bei FP-Zahlen gibt.