Der heutige Beitrag bleibt in der Rubrik “Wie funktionieren Computer” und soll ein wenig mit dem (Vor-)Urteil aufräumen, dass Computer besser als Menschen rechnen. Dass sie schneller sind, ist unbestreitbar – kein Rechenkünstler der Welt wird mehrere Millionen Berechnungen in der Sekunde schaffen. Aber wie sieht es mit der Genauigkeit aus, insbesondere im Bereich der reellen Zahlen?
Eins vorweg: Computer können in der Theorie natürlich so beliebig genau rechnen, wie sie wollen (und wie es ihr Speicher zulässt – die Zahl Pi wird vermutlich in keinen Computer der Welt passen). Allerdings erfordert das zusätzlichen Aufwand, der mit steigender Genauigkeit immer größer wird und damit natürlich immer mehr Zeit verbraucht. In modernen Computern kommt daher ein standardisiertes Verfahren zur Berechnung zur Anwendung, welches auf einer festen Bitkettenlänge beruht. Stehen für eine Zahl etwa 32 Bit zur Verfügung, so können damit insgesamt 232, also 4294967296 (rund 4 Milliarden) Zahlen dargestellt werden. Bei 64 Bit sind es dann schon gewaltige 18446744073709551616 (mehr als 18 Trillionen) Zahlen. Hält man sich hier nur im Bereich der natürlichen oder ganzen Zahlen auf, gibt es kein Problem, da jede der Zahlen im Wertebereich eindeutig dargestellt werden kann. Nach der 1 kommt nun einmal die 2, dazwischen ist nichts. Auch das Rechnen ist relativ einfach, wie wir hier und hier schon gesehen haben.
Bisher habe ich allerdings verschwiegen, wie am Computer all das dargestellt wird, was außerhalb des Wertebereichs der ganzen Zahlen liegt; konkret: die reellen Zahlen. Wer sich nicht mehr ganz erinnert: reelle Zahlen umfassen praktisch alles, was wir so im Alltagsgebrauch an Zahlen benutzen, angefangen von den natürlichen und ganzen Zahlen über die rationalen Zahlen (all die Zahlen, die als Verhältnis zweier ganzer Zahlen darstellbar sind) bis hin zu den irrationalen Zahlen (wozu auch Pi und e zählen). Da die reellen Zahlen in der modernen Mathematik unabdingbar sind und ein Großteil der Probleme, die mit Computern gelöst werden, auf diesem Zahlenbereich basieren, müssen sie natürlich auch effektiv im Rechner dargestellt werden können. Da wir bei der Darstellung auf Bitketten angewiesen sind, die nur aus 0en und 1en bestehen können, fällt die in der Mathematik übliche Darstellung der Zahlen mit einem Trennzeichen weg, es ist also ein anderes Verfahren notwendig. Bevor es jetzt gleich in die Details geht, noch eine Anmerkung: im deutschsprachigen Raum ist das Komma als (Dezimal-)Trennzeichen in reellen Zahlen üblich – im englischsprachigen Raum dagegen der Punkt. Da in der Informatik üblicherweise ein Punkt als Trennzeichen benutzt wird, halte ich mich auch hier daran; die Zahl “1.5” ist also für deutsche Augen zu lesen als “Eins Komma Fünf” – dennoch werde ich von einem “Komma” sprechen und nur bei der Darstellung den Punkt verwenden.
Das einfachste Verfahren zur Darstellung von reellen Zahlen durch Bitketten ist, ein Komma an einer bestimmten Stelle der Bitkette implizit anzunehmen, ohne es explizit hinschreiben zu müssen. Da das (gedachte) Komma hier an einer festen Stelle ist, spricht man auch von Festkommazahlen; die Bits vor der gedachten Kommastelle codieren hierbei entsprechend den Vorkommaanteil der Zahl, die Bits hinter der gedachten Kommastelle entsprechend den Nachkommaanteil. Die Umrechnung einer Bitkette in eine Festkommazahl ist relativ trivial: dazu wird die Bitkette zuerst als ganze Zahl interpretiert und dann durch den Wert 2f geteilt, wobei f die Anzahl der Bits hinter der gedachten Kommaposition bezeichnet. Ein Beispiel: angenommen, wir haben Bitketten der Länge 4, deren gedachte Kommaposition genau in der Mitte liegt; f wäre mithin 2, 22 also 4. Haben wir nun die Bitkette 1010 (mit Komma: 10.10) so stellt sie die in der Dezimaldarstellung die Zahl 10 dar (nämlich 1*8 + 0*4 + 1*2 + 0*1); geteilt durch 4 ergibt sich für die Bitkette 1010 also die Zahl 2.5. Der große Vorteil von Festkommazahlen ist, dass mit ihnen relativ einfach gerechnet werden kann. Der Nachteil dieser Darstellung ist allerdings offensichtlich: durch die feste Kommaposition ist der darstellbare Wertebereich sehr eingeschränkt. Aber Achtung: hier ist natürlich nicht die Anzahl der darstellbaren Zahlen gemeint – die beträgt weiterhin 232; vielmehr ist die Ausdehnung des Wertebereiches beschränkt, da für den ganzzahligen Anteil weniger Bits zur Verfügung stehen; die größte darstellbare Zahl ist also beschränkt.
Um diesem Problem zu begegnen, wurde die Darstellung in Form sogenannter Gleitkommazahlen entwickelt. Wie es der Name schon andeutet, ist hier die Position des Kommas nicht festgelegt, sondern wird in der Zahl selber codiert; durch dieses Vorgehen kann ein weitaus größerer Zahlenbereich abgedeckt werden (was zu anderen Problem führt, aber dazu gleich mehr). Mathematisch gesehen entspricht die Gleitkommadarstellung einer normalisierten Exponentialschreibweise. Wer sich nicht mehr erinnert: eine Zahl in Exponentialschreibweise lässt sich als m * be darstellen, wobei m die sogenannte Mantisse (die eigentliche Zahl), b die Basis (im Dezimalsystem die 10) und e der Exponent ist. Üblicherweise wird die Schreibweise zur Darstellung sehr großer oder sehr kleiner Zahlen verwendet, etwa 12000 = 12.0 * 103 oder 0.001 = 1.0 * 10-3. Von einer normalisierten Exponentialschreibweise spricht man, wenn sich vor dem Komma in der Mantisse lediglich eine einzelne Ziffer ungleich 0 befindet; 12000 müsste hierfür also als 1.2 * 104 geschrieben werden (es gibt auch noch andere Normalisierungsbedingungen, aber die sind hier nicht von Interesse). Im Computer lohnt sich hier nun natürlich, als Basis der Darstellung die 2 zu wählen. Durch die Forderung, dass die Zahl immer normalisiert sein soll, ergibt sich immer die folgende Darstellung (wobei p den Nachkommateil der Zahl beschreibt): 1.p * 2e. Die 1 vor dem Komma ergibt sich automatisch durch die erzwungene Normalisierung: vor dem Komma darf nur eine Ziffer ungleich 0 stehen; da im binären System nur 1 und 0 zur Verfügung stehen, ist die Wahl hier eindeutig.
Jetzt muss diese Zahl nur noch in einer Bitkette codiert werden. Da liegt es nun natürlich nahe, in einem Teil der Bitkette den Exponenten und im zweiten Teil der Bitkette die Mantisse zu hinterlegen. Der IEEE-Standard 754 für Gleitkommazahlen einfacher Genauigkeit (das heißt unter Ausnutzung von 32 Bits) sieht etwa vor, dass für den Exponenten 8 Bit und für die Mantisse 23 der 32 Bit zur Verfügung stehen. Die führende 1 der Mantisse muss übrigens nicht mitgespeichert werden, da sie ja definitiv immer vorhanden ist; es reicht also, sich auf den Nachkommateil zu beschränken. Das eine fehlende Bit (23+8=31) ist übrigens für das Vorzeichen reserviert, damit auch negative reelle Zahlen dargestellt werden können, wobei in der Regel eine 0 für positive Zahlen und eine 1 für negative Zahlen verwendet wird. Die Bits werden in der Reihenfolge Vorzeichen (s) – Exponent (e) – Mantisse (m) abgespeichert, so dass sich für eine 32-Bit-Zahl das folgende Muster ergibt:
s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm
Bleibt nur noch ein Problem: der Exponent kann positiv sein wie bei der Zahl
12.0 * 103 oder aber negativ wie bei der Zahl 1.0 * 10-3 (beide Zahlen der Einfachheit halber im Dezimalsystem). Bei der Speicherung des Exponenten muss also noch ein Vorzeichen untergebracht werden. Eine Möglichkeit wäre natürlich die bereits bekannte Darstellung im Zweierkomplement, welche allerdings aus gleich noch näher erläuterten Gründen nicht benutzt wird. Stattdessen wird der Exponent mit einem sogenannten Bias gespeichert. Der Bias ist ein vereinbarter, gedachter Wert, welcher bei der Konvertierung einer Zahl in die Bitkette auf den Exponenten aufaddiert wird. Bei einem Bias von zum Beispiel 127 (bei einer Exponentenlänge von 8 Bit) wird für einen Exponenten e = 3 also der Wert 130, für einen Exponenten e = -3 der Wert 124 abgespeichert.
Die Verwendung des Bias hat gegenüber dem Zweierkomplement einen entscheidenden Vorteil, da sie den Vergleich zweier Zahlen erheblich vereinfacht – in dieser Art der Darstellung reicht es, die Bitketten zweier zu vergleichender Zahlen einfach lexikografisch, das heißt Bit für Bit von links nach rechts (unter Ausschluss des Vorzeichenbits), miteinander zu vergleichen.
Damit können wir nun endlich ein kleines Beispiel anschauen. Nehmen wir dazu einmal als darzustellenden Wert die 23.625. Das Vorzeichenbit ist schnell gelöst, das ist nämlich 0 (da wir es mit einer positiven Zahl zu tun haben). Aber wie bringen wir den Rest in eine normalisierte Darstellung? Dazu wandelt man die Zahl zuerst in eine Festkommazahl um, und zwar Vor- und Nachkommateil getrennt voneinander. Die 23 lässt sich ziemlich einfach durch wiederholtes Dividieren durch 2 mit Rest umwandeln; folgende Tabelle zeigt die Rechnung, wobei der errechnete Rest den Vorkommateil in Binärdarstellung (in umgekehrter Reihenfolge) angibt:
23/2 | = 11, Rest: 1 |
11/2 | = 5, Rest: 1 |
5/2 | = 2, Rest: 1 |
2/2 | = 1, Rest: 0 |
1/2 | = 0, Rest: 1 |
Für den Nachkommateil muss man sich ein klein wenig mehr Mühe geben, aber das ist auch nicht sehr schwierig. Statt durch 2 zu teilen, multiplizieren wir einfach mit 2 und schauen, ob das Ergebnis größer als 1 ist; wenn ja, steht an der entsprechenden Stelle (diesmal in der “normalen” Reihenfolge von links nach rechts) eine 1 und man fährt mit der um 1 reduzierten Zahl fort, ansonsten steht eine 0 und es geht direkt weiter. Die Rechnung selber ist aber weitaus anschaulicher als eine Erklärung, daher hier die Tabelle:
0.625*2 | = 1.25, Bit: 1 |
0.25*2 | = 0.5, Bit: 0 |
0.5*2 | = 1.0, Bit: 1 |
0.0*2 | = 0.0, Bit: 0 |
… |
Insgesamt ergibt sich also die Festkommazahl 10111.10100000… als Repräsentation der 23.625; zur Kontrolle:
101112 = 1*24 + 0*23 + 1*22 + 1*21 + 1*20 = 16 + 4 + 2 + 1 = 23
und
101.2 = 1*2-1 + 0*2-2 + 1*2-3 = 0.5 + 0.125 = 0.625.
Zur Normalisierung muss das Komma in dieser Zahl nun um 4 Stellen nach links verschoben werden; das entspricht einer Multiplikation mit 24, ganz so wie im Dezimalsystem. Für die normalisierte Darstellung erhalten wir also: 1.011110100000 * 24. Die Mantisse ist also 101111012 und der Exponent 410 = 1002. Nun muss noch der Bias auf den Exponenten addiert werden; bei 32-Bit-Zahlen ist das in der Regel 127, also erhalten wir für den Exponenten 13110 = 100000112. Setzen wir alle gesammelten Zahlen zusammen (wir erinnern uns: die führende 1 der Mantisse kann ignoriert werden), ergibt sich demzufolge:
23.62510 = 0 10000011 011110100000000000000002
So schwer ist es also gar nicht. Gleitkommazahlen haben gegenüber Festkommazahlen den großen Vorteil, dass auch sehr große und sehr kleine Zahlen dargestellt werden können, indem der Exponent sehr groß beziehungsweise sehr klein wird. Ganz unproblematisch ist das allerdings nicht: je größer der Exponent ist, desto weniger Bits stehen zur Verfügung, um die niederwertigen Ziffern der Zahl darzustellen. Hat der Exponent zum Beispiel den Wert 23, so muss das (gedachte) Komma der gespeicherten Mantisse um 23 Stellen nach rechts geschoben werden – die dadurch entstehende Festkommazahl hat dementsprechend gar keine Nachkommastellen mehr. Je größer der Exponent wird, desto mehr niederwertige Stellen der Zahl gehen demzufolge verloren. Und genau hier liegt das eigentliche Problem der Gleitkommazahlen; mit steigender Größe der Zahlen (im positiven wie im negativen Bereich) werden auch die Lücken zwischen den darstellbaren Zahlen immer größer. Die Wikipedia bietet hierfür eine schöne Abbildung; gezeigt sind die darstellbaren Zahlen für verschiedene Mantissenlängen. Jeder Kreis markiert eine Zahl, die als Gleitkommazahl nach dem vorgestellten Schema dargestellt werden kann. Es ist leicht zu sehen, dass insbesondere bei kurzer Mantissenlänge die Abstände zwischen den Zahlen immer größer werden (bei größerer Mantissenlänge verschiebt sich das Problem einfach in höhere Wertebereiche).
Kommentare (12)