Gleitkommazahlen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Gleitkommazahlen (auch Fließkommazahlen oder englisch floating point number 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. Auf vielen Mikrocontrollern, wie den AVRs, gibt es keine FPU, was die Berechnungen verhältnismäßig langsam macht. Größere Mikrocontroller, beispielsweise die ARM Cortex-M4F, besitzen daher typischerweise solche Zusatz-Hardware.

Darstellung von Zahlen

Anders als von Ganzahlen (Integern) gewohnt, werden die Zahlen nicht einfach im Zweierkomplement 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:

  1. Umrechnen: Vom Dezimalsystem ins Dualsystem konvertieren. 12,7510 -> 1100,11
  2. Normieren: Das Komma wird so weit verschoben, bis es genau eine führende 1 gibt. 1,10011
  3. 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|
+-+--------+-----------------------+

Als Faustregel kann man mit 7 bzw. 15 korrekten signifikanten Stellen bei float bzw. double rechnen.

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 vorkommen können und somit behandelt 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 (was bei C und verwandten Sprachen durch einen Operand vom Typ float oder double auf der rechten Seite erzwungen wird).

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

  • Gleitkommazahlen bilden keine reellen Zahlen, sondern nur rationale Zahlen ab. Dabei werden die genau darstellbaren Zahlen bei größeren Beträgen immer seltener.
  • 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

Hinweise

Bei alten (und oftmals heute noch genutzten) Versionen des avr-gcc (<10) wird sowohl float, als auch double mit 32bit gespeichert und gerechnet. Damit verhalten sich diese älteren avr-gcc-Versionen nicht Standard-konform, sparen aber auch Speicher und Rechenzeit (offensichtlich zugunsten von ungenaueren Ergebnissen). Ab Version 10 kann avr-gcc 64bit double. Für portablen Code kann man das #define __SIZEOF_DOUBLE__ verwenden, um die Anzahl Bits zu prüfen. Siehe dazu die Release Notes des GCC.

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 Problemstellungen 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.

Literatur