Gleitkommazahlen
Gleitkommazahlen sind eine Möglichkeit für Computer und Mikrocontroller mit rationalen Zahlen (also Brüche und Kommazahlen) zu rechnen. Die damit verbundene Gleitkommaarithmetik ist das Gegenstück zur Festkommaarithmetik.
Um effizient mit Gleitkommazahlen zu rechnen empfiehlt sich ein Controller mit eingebauter FPU, da dieser Hardware-Befehle zum Umgang mit diesen besitzt. Sonst muss die Toolchain alle Gleitkommaoperationen mittels mehrerer Hardwarebefehle nachbilden, was Zeit und Speicher kostet.
Darstellung von Zahlen
Anders als gewohnt werden die Zahlen nicht einfach binär gespeichert, sondern nach dem IEEE-754-Format:
x = (-1)s * m * be
- x ist die gewünschte Zahl im Gleitkommaformat
- s ist das Vorzeichen-bit: 0 entspricht +, 1 entspricht -
- m ist die sogenannte Mantisse, sie speichert die Ziffern der Zahl
- b ist die Basis 2 und
- e ist der Exponent, dieser gibt die Position des Kommas an
Die Anzahl der Bits für Exponent und Mantisse hängen von der gewünschten Genauigkeit ab. Die Programmiersprache C (und die davon abgeleiteten) kennen folgende Genauigkeiten:
Datentyp | Anzahl Bits | Sign | Mantisse | Exponent | Anmerkung |
---|---|---|---|---|---|
float | 32 Bit | 1 bit | 23 bit | 8 bit | single precision |
double | 64 Bit | 1 bit | 52 bit | 11 bit | double precision
Achtung: Abweichend vom Standard sind double beim AVR-GCC und der avr-libc auch nur 32 Bit! |
long double | 80 Bit | 1 bit | 64 bit | 15 bit | extendend precision
Achtung: Der C Standard legt nicht genau fest, wie long double zu implementieren ist. Diese Angaben sind also nicht allgemein gültig. |
Gespeichert wird in der folgenden Reihenfolge:
+------+----------+----------+
| Sign | Exponent | Mantisse |
+------+----------+----------+
Wobei der Exponent rechtsbündig ist, die Mantisse dagegen linksbündig. Das LSB steht immer rechts, das MSB links.
Berechnung von Hand
Als Beispiel soll die Zahl -12,75 dienen. Diese soll ins IEEE754-single-precision-Format gewandelt werden.
Zuerst muss man die Mantisse berechnen: Das funktioniert in 3 Schritten:
- Umrechnen: Vom Dezimalsystem ins Dualsystem konvertieren. 12,7510 -> 1100,11
- Normieren: Das Komma wird so weit verschoben, bis es genau eine führende 1 gibt. 1,10011
- Mantisse: die führende 1 Kommt bei jeder Zahl (mit Ausnahme von den Spezialfällen) vor, also ist sie redundant und wird nicht gespeichert. Somit ist das Bitmuster unserer Mantisse 10011
Der Exponent ist eigentlich auch ganz einfach: dieser gibt an, um wie viele Stellen wir das Komma verschoben haben, im Beispiel also 3. Allerdings wird im Exponent nicht nur eine 3 gespeichert, denn dann wären Negative Exponenten nicht (so einfach) möglich. Deswegen wird der Exponent 0 einfach auf die Hälfte minus 1 der maximal darstellbaren Zahl festgelegt, bei float mit einem 8-bit-Exponent also auf 256 / 2 -1 = 127. Dies nennt sich Bias. Bei double ist dieser Bias dementsprechend 1023. Um nun den "richtigen" wert des Exponenten herauszubekommen muss man nun diesen Bias zu unserem Wert 3 addieren, was 130 ergibt.
Nun muss man nur noch die Ergebnisse zusammenbasteln:
- Die Zahl ist negativ, also ist das Sign-Bit gesetzt
- Der Exponent ist 13010 bzw. 1000 00102
- Die Mantisse ist 100112 und wird linksbündig gespeichert.
+-+--------+-----------------------+
|S|Exponent| Mantisse |
+-+--------+-----------------------+
|1|10000010|10011000000000000000000|
+-+--------+-----------------------+
Vorteile
- hoher Dynamikbereich: je nach dem wie viele Bits genutzt werden können sehr sehr kleine und sehr große Werte dargestellt werden
- Extra definierte Zahlen für -INF (minus Unendlich), +INF (plus Unendlich) und NaN (not a number), die bei Rechenoperationen herauskommen können und somit geprüft werden können. So sind (immer bei Gleitkommarechnungen, nicht Integer) eine Zahl (ungleich 0) / 0 immer INF (+ oder - je nach dem ob die Zahl größer oder kleiner 0 war). 0/0 entspricht immer NaN. Bei Integerrechnungen sind diese Operationen undefiniert.
- Formeln können mehr oder weniger komplett und direkt übernommen werden. Es muss nur darauf geachtet werden, dass die Operationen auch in float/double ausgeführt werden.
Ein kleines Beispielprogramm:
#include <stdio.h>
int main() {
double a = 5 / 3;
printf("5 / 3 = %.10f (Berechnung mittels int)\n", a);
double b = 5.0 / 3.0;
printf("5 / 3 = %.10f (Berechnung mittels double)\n", b);
double c = (double)5 / 3;
printf("5 / 3 = %.10f (Berechnung mittels cast auf double)", c);
return 0;
}
Dieses hat die folgende Ausgabe:
5 / 3 = 1.0000000000 (Berechnung mittels int)
5 / 3 = 1.6666666667 (Berechnung mittels double)
5 / 3 = 1.6666666667 (Berechnung mittels cast auf double)
Nachteile
- Unterläufe von sehr kleinen Zahlen auf 0
- Auslöschung bei Subtraktion: Bei zwei fast gleich großen Zahlen wird das Ergebnis falsch
- Prüfen auf Gleichheit: siehe hier
- Absorption, dazu wieder ein kleines Beispiel:
#include <stdio.h>
int main() {
float little = 0.0000001F;
float huge = 100000.0000000F;
printf("little = %17.10f\n", little);
printf("huge = %17.10f\n", huge);
printf("little + huge = %17.10f", little + huge);
return 0;
}
Dieses hat die folgende Ausgabe:
little = 0.0000001000
huge = 100000.0000000000
little + huge = 100000.0000000000
Wie man sehen kann: Durch die viel kleinere Zahl und den damit verbundenen Ungenauigkeiten bei der Rechnung ändert die Rechnung nichts.
- Ungenaue Darstellung von Zahlen
#include <stdio.h>
int main() {
// ich habe dieses Beispiel gewählt, weil meine "tolle und hochgenaue" Taschenrechner App dieses Ergebnis ausgab
printf("6,4 - 6,85 = %.15f", 6.4 - 6.85);
return 0;
}
Ausgabe:
6,4 - 6,85 = -0.449999999999999
Sollte ja eigentlich -0,45 sein. Aber weil der Computer mit der Basis 2, wie Menschen im allgemeinen mit der Basis 10 rechnen, sind diese für uns einfachen Zahlen für einen Computer eben nicht genau darzustellen. Und dann passieren bei Rechnungen solche Fehler
Zusammenfassung
Gleitkommazahlen sind kein Allheilmittel, im Gegenteil. Sie sollten nur mit bedacht eingesetzt werden. Das Problem ist, das viele Leute gar nicht wissen, was Gleitkommazahlen eigentlich sind und vor allem wie sie Aufgebaut sind.
Bei Problemen mit kleinem oder gar keinem Dynamikbereich ist oftmals Festkommaarithmetik die bessere Wahl.
Wenn man Gleitkommazahlen jedoch mit bedacht einsetzt und immer die möglichen Fehler im Kopf hat, dann sind diese sehr mächtig.