Ein wenig Theorie zu den Fließkommazahlen
in der Softwaretechnik mit C und C++

Es hat sich meiner Erfahrung nach herum­gesprochen, dass die Verwendung von Fließkommazahlen – auch Gleitkommazahlen genannt – in der Software­entwicklung nicht immer ganz problemfrei ist. Mancher Programmierer scheut deshalb deren Verwendung wie der Teufel das Weihwasser. Andere Entwickler verwenden die entsprechenden Datentypen float und double etwas naiver und reagieren erst wenn die Software zu erkennen gibt, dass sie nicht so laufen möchte, wie es sich der Urheber eigentlich dachte. Doch was ist nun eigentlich das Problem mit diesen Zahlen?

#include <stdio.h>
int main()
{
      double d1 = 1.2;
      double d2 = 0.8;
      d2 += 0.4;
      puts( ( d1 == d2 ) ? "gleich" : "ungleich" );
      return 0;
}
Listing 1: Beispiel in C zur Ungenauigkeit eines
errechneten Fließkommawertes.

Das Darstellungsproblem

Einfache Vergleiche von errechneten Fließkommazahlen unter Verwendung der Vergleichs­operatoren in C und C++ führen oft nicht zum erwarteten Ergebnis. Das C-Beispiel im nebenstehenden Kasten lässt sich mit jedem Standard−C Compiler übersetzen. Zum Beispiel mit dem C Compiler unter UNIX/Linux:

cc bsp1.c -o bsp1
Der Aufruf ./bsp1 zeigt ungleich.

Das liegt daran, dass die Werte 1.2, 0.8 und 0.4 ebenso wie die meisten Zahlen binär überhaupt nicht exakt darstellbar sind. Es werden Werte codiert, die knapp neben den erwarteten Ergebnissen liegen. Generell kann gesagt werden, dass keine Darstellungs­form von reellen Zahlen in der Lage ist, alle Zahlen abzubilden. Auch nicht die dezimale Form, die wir für gewöhnlich im Alltag verwenden. Zwischen zwei darstellbaren Zahlen liegen immer unendlich viele nicht darstellbare. Dieses grund­sätzliche Problem ist also keine Spezialität der Binärdarstellung in Computer­systemen. Man kann die meisten Zahlen bestenfalls als Näherung in ausreichender Genauigkeit darstellen und nur wenige exakt. Darüber hinaus gibt es noch viele Zahlen, die im Dezimalsystem exakt darstellbar sind, im Binärsystem jedoch so viele Nachkomma­stellen haben, dass sie in den gegebenen 32 oder 64 Bit nicht darstellbar sind. Manche im Dezimalsystem mit endlichen Ziffern darstellbare Zahlen haben im Binärsystem sogar unendlich viele Nachkomma­stellen. Die darstellbaren Zahlen des Dezimalsystems sind also nicht komplett auf das Binärsystem abbildbar und man muss mit Näherungswerten arbeiten. Weiteres ist im Abschnitt über die Ungenauigkeit der Fließkomma­darstellung beschrieben.

Die technische Darstellung von Fließkommazahlen in C und C++

Die in C und C++ gebräuchlichen Gleitkommatypen float und double sind im Standard IEEE 754 definiert.

Darin ist geregelt, dass eine Zahl z nach dem Prinzip z = m × 2e dargestellt wird. Wobei m die Mantisse und e der Exponent ist. Der Datentyp float hat 32 Bit, die sich in eine Mantisse mit 23 Bit, einen Exponenten mit 8 Bit und ein Vorzeichenbit aufteilen. Diesen Datentyp bezeichnet man auch als Fließkommatyp mit einfacher Genauigkeit.

Bei double sieht die Aufteilung genauso aus. Nur die Breiten ändern sich gegenüber float. Die Mantisse hat 52 und der Exponent 11 Bit. Zusammen mit dem Vorzeichenbit kommen wir auf eine Größe von 64 Bit. Der Datentyp double kann Fließkommawerte mit doppelter Genauigkeit darstellen.

Es gibt mit long double in den C und  C++ Standards noch einen breiteren Datentyp für die Darstellung von Gleitkommawerten. Allerdings ist dieser Datentyp nicht strikt definiert und kann auf unter­schied­lichen Plattformen oder bei der Verwendung unter­schied­licher Compiler in seiner Darstellung variieren. Übliche Darstellungs­formate für long double sind beispiels­weise das 80 Bit breite Extended Precision Format oder das 128 Bit breite Quadrupel Format aus dem Standard IEEE 754.
Da die technische Darstellung dieses Datentyps nicht einheitlich definiert ist, verzichte ich in diesem Artikel auf eine genauere Beschreibung. Gleichwohl gilt die Beschreibung des Problem­bereichs der Fließkomma­darstellung grundsätzlich auch für diesen breiteren Typ. Auch das Prinzip der Codierung ist technisch analog zu den beiden Datentypen float und double gelöst.

Wie werden nun die Gleikommazahlen codiert?

Die Normalisierung von Fließkommawerten

Wenn man Werte binär so darstellt, dass sie vor dem Komma genau eine Stelle mit der Eins haben, wird diese Darstellung normalisiert genannt. In der Binärdarstellung steht also immer die Eins an der ersten Stelle wenn es sich um den sogenannten normalisierten Zahlenbereich handelt und eine Null wenn es der denormalisierte Bereich ist. Der Wert 0.0 kann deshalb nicht normalisiert werden. Aber bei den meisten Zahlen, die in der Exponenten­darstellung darstellbar sind, ist eine Normalisierbarkeit möglich. Man verschiebt dabei das Komma mit Hilfe des Exponenten so, dass nur eine Stelle davor übrig bleibt. Die normalisierten Zahlen bewegen sich für float im Bereich FLT_MIN bis FLT_MAX im positiven, wie auch im negativen Bereich. Um die 0.0 herum ist der Bereich der nicht normalisierten oder eben denormalisierten Werte. Für double gibt es die Konstanten DBL_MIN und DBL_MAX.

Durch das Zusammenwirken von Mantisse und Exponent wird ein Wert dargestellt der durch das Vorzeichenbit nur noch mit einem Vorzeichen versehen wird. Der Exponent verschiebt einfach nur das Komma – Im binären Zahlensystem natürlich. Dabei gibt es noch eine Besonderheit bezüglich des Exponenten.

Die Verschiebedarstellung des Exponenten

Der Exponent wird in einer sogenannten Verschiebedarstellung repräsentiert (engl. Biased Representation). Dazu wird ein Verschiebewert – Bias oder Exzess – aufaddiert. Wenn man also den echten Exponenten eines Wertes erhalten möchte, muss man den entsprechenden Bias vom dargestellten Exponentenwert abziehen. Der Bias für den Exponenten errechnet sich aus 2n−1−1 für n = Bits des Exponenten in der binären Darstellung. Der Bias für float ist 27−1 = 127. Für double ist der Bias 210−1 = 1023.

Die Verschiebedarstellung führt dazu, dass sich Gleitkommazahlen bei größer/kleiner-Vergleichen wie ganze Zahlen vergleichen lassen. Der Exponent wird damit in eine Form gebracht, in der sein direkter Betrag verglichen werden kann. Bei der Mantisse ist das sowieso schon der Fall. Da im Bitmuster der Exponent die Mantisse nach vorne erweitert, können beide zusammen genommen direkt verglichen werden. Die Zahl, bei der am weitesten vorne eine 1 steht, während die andere an der gleichen Position eine 0 hat, ist die größere. Das ist technisch als Hardware­operation sehr einfach zu realisieren und deshalb auch sehr performant. Es entspricht einem Vergleich zwischen zwei ganz­zahligen Werten.

Sonderfälle

Die denormalisierten Zahlen

Wenn der Exponent nur aus Nullen besteht, spricht man von denormalisierten Zahlen (engl. subnormal). In diesem Fall wird keine 1 vor dem Komma angenommen. Außerdem wird mit dem Exponenten −126 für float und −1022 für double gerechnet.

#include <stdio.h>
#include <math.h>
int main()
{
      if( NAN != NAN )
      {
          puts( "NAN != NAN" );
      }
      return 0;
}
Listing 2: Beispiel in C zum Vergleich mit NaN.
#include <stdio.h>
#include <float.h>
#include <math.h>

int main()
{
    double d = 0.0 * INFINITY;
    if( isnan(d) )
    {
        puts( "isnan(d)" );
    }
    if( d != d )
    {
        puts( "d != d" );
    }
    return 0;
}
Listing 3: Beispiel in C zur Prüfung auf NaN.

Die Null

Die 0 ist eine denormalisierte Zahl und kann mit beiden Vorzeichen dargestellt werden.

+0: 0 00000000000 0000000000000000000000000000000000000000000000000000
−0: 1 00000000000 0000000000000000000000000000000000000000000000000000


Not a Number – NaN

Wenn der Exponent nur aus Einsen besteht und die Mantisse mindestens eine Eins enthält so handelt es sich um eine Darstellung für NaN. Das Vorzeichenbit spielt für die Darstellung keine Rolle. Undefinierte Rechen­operationen können NaN als Resultat haben. Es gilt außerdem die Regel, dass jeder Vergleich mit NaN false ergibt − auch ein Vergleich auf sich selbst. Das nebenstehende C-Programm hat die Ausgabe NAN != NAN. In der C-Headerdatei math.h und dem C++-Pendant cmath ist die Konstante NAN definiert.

NAN0 11111111111 0000000000000000000000000000000000000000000000000001


Die Darstellung von Unendlich

Divisionen durch Null haben das Ergebnis Unendlich. Je nachdem ob eine positive oder negative Zahl durch Null geteilt wurde ergibt sich ±Unendlich. In der beschriebenen Gleitkomma­darstellung besteht der Exponent nur aus Einsen und die Mantisse nur aus Nullen. In math.h ist dafür die Konstante INFINITY definiert.

+0 11111111111 0000000000000000000000000000000000000000000000000000
1 11111111111 0000000000000000000000000000000000000000000000000000

Binäre Darstellung von beliebigen Gleitkommazahlen...

Dezimale Zahl:  

Die Ungenauigkeit in der Darstellung von Fließkommawerten

Wie im Abschnitt über das Darstellungs­problem von Fließkomma­werten schon beschrieben wurde, kann nur eine endliche Anzahl verschiedener Werte überhaupt dargestellt werden – gegenüber einer unendlichen Anzahl nicht darstellbarer Werte. Das heißt auch dass Werte, die aus einer Berechnung entstehen möglicher­weise nicht exakt darstellbar sind. Mit hoher Wahrschein­lichkeit sogar. Natürlich hängt das von den verwendeten Operanden einer Operation und von der Operation selbst ab. Die Mehrheit der Rechen­ergebnisse wird aber nicht exakt darstellbar sein. Dabei muss auch die Konvertierung aus einer Fließkomma­darstellung im Dezimal­system zum binären System als Operation verstanden werden. Daraus folgt, dass sogar Konstanten in einem Quellcode, die ja in eine binäre Darstellung überführt werden müssen, nicht immer exakt darstellbar sind. Das Codebeispiel in Listing 1 ist ein gutes Beispiel dafür: die dezimal dargestellte Zahl 1.2 ist im binären Exponential­format nicht exakt darstellbar. Sie kann nur näherungs­weise repräsentiert werden. Da hilft auch die doppelt Präzision von double nichts. Wie kann also diese Ungenauigkeit erfasst und verstanden werden?

Dazu muss man verstehen, dass die Distanz der darstell­baren Zahlen auf dem Zahlenstrahl zueinander nicht immer gleich ist. Diese Distanz nimmt mit der Größe des Betrags zu. Die geringsten Abstände zwischen den darstellbaren Werten sind also direkt um die 0 herum. Nach außen vergrößern sie sich, wie auf der folgenden Abbildung schematisch dargestellt:

                0       +          
· · · · · · · · · · · · · · · · · · · · ·

Zwischen den darstellbaren Zahlen, befinden sich immer unendlich viele nicht-darstellbare. Wie können diese Lücken nun mathematisch gefasst werden und wie geht man praktisch mit ihnen um wenn man Fließkommatypen in der Software benötigt?

Das Epsilon als das numerische Maß der Ungenauigkeit

Die Ungenauigkeit in der Darstellung von Fließkomma­werten erfasst man mit einem Wert mit dem Namen Epsilon. Das Epsilon ist ein relatives Maß für die Darstellungs­ungenauigkeit. Das Epsilon das die Ungenauigkeit um die Zahl 1.0 herum beschreibt nennt man Standardepsilon. Im IEEE 754 Standard ist das Standard­epsilon der kleinste Wert für den 1.0 + eps != 1.0 ohne Rundungs­fehler gilt (rein technisch könnte auch eine Zahl von nahezu der Hälfte des Standard­epsilons auf die 1.0 auf­addiert werden und mit Hilfe des Rundungs­fehlers zu einer Werte­änderung führen). Das Standard­epsilon definiert also die Differenz von 1.0 zur nächstg­rößeren darstell­baren Zahl. Da man aber mit Gleit­komma­daten­typen Zahlen völlig unterschied­licher Größen­ordnungen mit der gleichen Anzahl an binären Stellen anzeigen möchte, skaliert die Ungenauig­keit mit der Größe der dargestellten Zahlen. Die beiden Zahlen 0.0001 und 100000.0 haben beispiels­weise völlig unterschiedliche Epsilons. Um das Epsilon für eine bestimmte Zahl zu ermitteln, muss man das Standard­epsilon eps mit der Zahl multiplizieren. Damit erhält man das skalierte Epsilon für die ent­sprech­ende Zahl:
eps100000 = 100000 × eps
Für eine beliebige Zahl n gilt also:
epsn = n × eps
Die Darstellung des Ergebnisses n einer einzigen mathematischen Operation kann also in einer Distanz von epsn um das tatsächliche Ergebnis herum liegen. Die absolute Abweichung der Darstellung vom exakten Ergebnis variiert also mit der Größen­ordnung von n und wird durch das skalierte Epsilon beschrieben.

Möchte man errechnete Fließ­kommawerte miteinander vergleichen, muss man den möglichen entstandenen Fehler in Form des Epsilons berücksichtigen. Man errechnet sich also das Epsilon für die entsprechenden Zahlen und macht Bereichs­überprüfungen. Für float, double und long double gibt es in der Standard­bibliothek in der Headerdatei float.h vor­definierte Standard­epsilons:

In float.h definierte
Konstante für Standardepsilon
floatFLT_EPSILON
doubleDBL_EPSILON
long doubleLDBL_EPSILON

Das ULP als das technische Maß der Ungenauigkeit

ULP ist eine Abkürzung von „Unit in the Last Place“ und bezeichnet das am niedrigsten signifikante Bit für die Zahlen­darstellung in der Mantisse. Also einfach das letzte Bit. Wenn ein Ergebnis einer Operation nicht darstellbar ist entscheidet es sich in diesem letzten Bit, ob die dargestellte Zahl gewissermaßen rechts oder links des wirklichen Ergebnisses liegt. Ob also das dargestellte Ergebnis größer oder kleiner ist. Natürlich hängt es von der Implemen­tierung der Operation ab, ob der kleinere oder größere Wert als Ergebnis genommen wird. Außerdem kann sich natürlich mehr an dem Bitmuster der Zahl ändern wenn man die nächste darstellbare Zahl erreicht.

Das ULP beschreibt also die Distanz zwischen zwei benachbarten darstell­baren Zahlen. Um das nächste ULP zu berechnen wird ein ein ganz­zahliges Inkrement oder Dekrement auf die Mantisse ausgeführt. Ein gefülltes oder geleertes Bitmuster führt natürlich zu einem Kippen und einer Änderung des Exponenten. Die Entfernung zwischen den beiden Zahlen ist aber immer noch ein ULP. Das ULP ist also die kleinst­mögliche Differenz zwischen zwei darstellbaren Zahlen. Betrachtet man das Bitmuster der Darstellung von Fließkomma­werten ist das ULP ein technisch einfach verständ­licher Begriff. Er führt zu genau der relativen Charakter­istik der Darstellung wie das Epsilon im vorange­gangenen Abschnitt. Welche mathematische Distanz ein ULP bedeutet entscheidet sich natürlich am Exponenten. Damit ist die Beschreibung der Ungenauigkeit über das ULP nur ein anderer Weg, um die gleiche Sache, die Abweichung der Genauigkeit in der Fließkomma­darstellung, zu beschreiben. Das ULP, angewandt auf eine spezifische Zahl entspricht dem skalierten Epsilon.

Der Zusammenhang von Epsilon und ULP

Das Standardepsilon ist genau so definiert, dass es bei der Addition auf 1.0 zum nächsten ULP von 1.0 führt. Es ist also exakt die Distanz zur nächsten darstell­baren Zahl. Skaliert man das Epsilon durch die Multiplikation mit einer Zahl im normalisierten Bereich, wird durch die Addition des skalierten Epsilons auf die Zahl das nächste ULP erreicht.
Es gilt also:
n + n × eps = nächstes ULP von n aus.
Die gleiche Regel gilt auch für die Substraktion skalierter Epsilons.

Der genaue Aufbau des Zahlenstrahls von double

Um es jetzt noch etwas genauer zu machen: zwischen zwei Zweierpotenzen gilt das gleiche Epsilon. Die Distanzen zwischen den darstell­baren Zahlen in einer solchen Binade – vgl. den Begriff der Dekade – sind identisch. Das Standard­epsilon gilt in der Binade 0. Das ist die Binade zwischen 1 und 2: [1.0, 2.0). Die Binade ist ein halboffenes Intervall mit der Menge aller Fließkommazahlen, die das gleiche Vorzeichen und den gleichen Exponenten besitzen: [2e, 2e+1). Die Schreibweise mit '[' und ')' bezeichnet das halboffene Intervall. Die erste Zweierpotenz gehört dazu, die zweite und höhere ist bereits außerhalb und begrenzt das Intervall von Außen.

In der Binade 0, also [1.0, 2.0) gilt, wie gesagt, das Standard­epsilon als Distanzmaß zwischen zwei darstellbaren Zahlen. In der darauf folgenden Binade Richtung höherer Zahlen, der Binade 1, also [2.0, 4.0), gilt das verdoppelte Standard­epsilon. Die nächsthöhere Binade verdoppelt das Epsilon der tieferen wieder. Das Prinzip wird bis zur höchsten Binade 1023, also [21023, 21024), durch­gehalten. Jede Binade enthält genau die gleiche Anzahl darstell­barer Zahlen. Da die Mantisse 52 Bits lang ist, kann eine Binade genau 252 = 4.503.599.627.370.496 Zahlen darstellen (4,5 Billiarden). Die Distanz der darstellbaren Zahlen in der Binade [1.0, 2.0) ist also 2-52. Genau das ist der Wert des Standard­epsilons. Die Binade [2.0, 4.0) ist doppelt so groß, enhält aber die gleiche Anzahl darstell­barer Zahlen. Deshalb muss das Epsilon skalieren, es wird doppelt so groß: 2-51. Das heißt auch, dass in der Binade 52, also [252, 253), ein skaliertes Epsilon mit dem Wert 20, also 1, erreicht wird. Ab dieser Binade können keine Nachkomma­stellen mehr dargestellt werden. Der allergrößte Bereich der darstell­baren Zahlen mit double lässt deshalb keine Nachkomma­stellen zu. Die Lücken zwischen den Zahlen, also die skalierten Epsilons werden ab der Binade 52 größer als 1. Jede ganz­zahlige Darstellung ist ab 252 genauer! Die Lücken werden in Richtung großer Zahlen riesig. Nicht gerade das, was die Intuition bei naiver Verwendung von Fließkomma­zahlen erwarten würde.

Die Abweichung denormalisierter Zahlen

Wir haben im normalisierten Darstellungsbereich 2046 Binaden. Die kleinste ist [2-1022, 2-1021), die größte [21023, 21024). Nach unten sind die Binaden durch die Darstellbarkeit eines skalierten Epsilons begrenzt. Der Exponent der Fließ­kommazahl hat 11 Bits. Er kann also 211 = 2048 Zahlen darstellen. Durch den in einem vorangegangenen Absatz beschriebenen Bias von 1023 stellt er einen Wert im Bereich [-1022, 1023] dar oder den Wert 2047 für NaN. Unterhalb von -1022 verliert der Exponent seine Funktion und es kann keine Fließkommatzahl mit der ersten binären Ziffer 1 dargestellt werden. Die subnormalen Zahlen haben deshalb an erster Stelle eine 0 und erben das kleinste darstellbare skalierte Epsilon aus der kleinsten Binade -1022.

Die Wertebereiche der Fließkommatypen

Denormalisierter Bereich
außer der 0
Normalisierter Bereich,
effektiver Darstellungsbereich
float ± { 2−149 bis (1 − 2−23) × 2−126} ± { 2−126 bis (2 − 2−23) × 2127}
double ± { 2−1074 bis (1 − 2−52) × 2−1022} ± { 2−1022 bis (2 − 2−52) × 21023}

Konsequenzen der Ungenauigkeit von Fließkommawerten in der Codierung

Vergleiche zwischen Fließ­komma­werten müssen die Ungenauig­keit der Darstellung berücksichtigen. Ein direkter Vergleich zweier Werte ohne die Abweichung mit einzubeziehen würde in sehr vielen Fällen zu falschen Resultaten führen. Listing 1 in diesem Artikel zeigt einen solchen fehlschlagenden Vergleich. Werden zwei berechnete Fließ­komma­werte verglichen, muss das skalierte Epsilon mit in den Vergleich einfließen, da davon ausgegangen werden muss, dass das Ergebnis nicht exakt dargestellt werden kann und damit um ein Epsilon abweicht.

#include <stdio.h>
#include <float.h> // fuer DBL_EPSILON
#include <math.h>  // fuer fabs()
int main()
{
    double d1 = 1.2;
    double d2 = 0.8;
    d2 += 0.4;
    double e = DBL_EPSILON * fabs(d1);
    puts( ( fabs(d1-d2) <= 2.0*e ) ? "gleich" : "ungleich" );
    return 0;
}
Listing 4: Beispiel in C zum korrekten Vergleich
zweier Gleitkommawerte.

Wenn zwei errechnete Werte verglichen werden, muss die doppelte skalierte Epsilon­distanz angenommen werden, da beide Werte in entgegen­gesetzter Richtung abweichen können. Wenn man die Differenz bildet, kann diese Differenz gegen den doppelten skalierten Epsilonwert verglichen werden. Das Standard­epsilon ist als Konstante DBL_EPSILON in der Headerdatei float.h definiert und kann durch eine einfache Multi­plikation mit einem der beiden zu vergleichenden Werte skaliert werden. Wenn diese Werte sich wesentlich unterscheiden, fällt das Epsilon nicht ins Gewicht. Wenn sie in direkter Nachbarschaft sind, ist die jeweils andere durch das skalierte Epsilon erreichbar. Wenn beide Werte aus einer Berechnung hervorgegangen sind kann man das Epsilon auch mit dem darstellungsmäßig größeren Wert multiplizieren, um bei doppelter Distanz ein zu kleines Epsilon zu vermeiden. Dann würde die Skalierung so aussehen:

double e = DBL_EPSILON * fmax( fabs(d1), fabs(d2) );

Das Codebeispiel in Listing 4 macht den Vergleich korrekt und prüft ob die beiden verglichenen Werte nahe genug beieinander stehen. Nämlich in einer Distanz von maximal zwei skalierten Epsilons oder eben von zwei ULPs.

Vergleiche von Gleitkommawerten

Nicht nur der Test auf Gleichheit muss das Epsilon berück­sichtigen. Auch der Größer- und Kleiner­vergleich muss abgesichert werden. Anstatt zwei Werte einfach mit dem Vergleichs­operator zu vergleichen, muss vorher das Epsilon in richtiger Weise addiert oder Subtrahiert werden. Falsch wäre if( d1 < d2 ).. , da die beiden Werte so nahe beieinander liegen können, dass sie als gleich betrachtet werden müssten. Der Operator jedoch prüft nur die tatsächlich dargestellten Zahlen ohne die Ungenauigkeit der Darstellung des Ergebnisses des vorange­gangenen Rechenwegs in Betracht zu ziehen. Das Epsilon muss also in den Vergleich mit einbezogen werden: if( ( d1 + e ) < d2 ).. oder eben if( ( d1 + e ) < ( d2 - e ) ).. , wenn beide Werte aus Berechnungen hervor­gegangen sind und damit die doppelte Epsilon­distanz berücksichtigt werden muss.

Vorsicht Falle! Die Operatoren „<=“ und „>=

#include <stdio.h>
#include <float.h> // fuer DBL_EPSILON
#include <math.h>  // fuer fabs()
int main()
{
    double d1 = 1.2;
    double d2 = 0.8;
    d2 += 0.4;
    double e = DBL_EPSILON * fabs(d1);
    puts( ( fabs(d1-d2) <= 2.0*e ) ? "gleich" : "ungleich" );
    puts( ( (d1+e) < (d2-e) ) ? "kleiner" : "nicht kleiner" );
    // Fehler:
    puts( ( (d1+e) <= (d2-e) ) ? "kleiner-gleich"
                               : "nicht kleiner-gleich" );
    return 0;
}
Listing 5: Beispiel in C mit fehlerhafter Anwendung des Operators „<=“.

Der nebenstehende Code enthält einen Fehler. Er produziert die Ausgabe:

gleich
nicht kleiner
nicht kleiner-gleich


Das ist natürlich nicht konsistent, denn wenn Werte gleich sind, dann sind sie formal auch kleiner-gleich. Die erwartete Ausgabe wäre also:

gleich
nicht kleiner
kleiner-gleich


Der Kleiner-Gleich-Operator „<=“ kann nicht naiv angewendet werden, als wäre er ein Kleiner-Operator „<“. Bei einem Kleiner-Vergleich wird das Epsilon so angewendet, dass der getestete angenommern kleinere Wert vergrößert wird. Er könnte aber bereits in seiner Darstellung größer sein. Die Addierung des Epsilong kann den Wert also aus dem Bereich bringen, in dem Gleichheit angenommen werden kann. Wenn also ein logischer Kleiner-Gleich-Vergleich gebraucht wird muss er aus einem logischen Kleiner-Vergleich und der logischen Überprüfung auf Gleichheit zusammen­gesetzt werden. Eine mögliche korrekte Lösung wäre also die folgende:

Download des korrigierten
Codes aus Listing 5.

puts( ( (d1+e) < (d2-e) || fabs(d1-d2) <= 2.0*e )
      ? "kleiner-gleich" : "nicht kleiner-gleich" );

Die Bedingung des Vergleichs besteht aus zwei veroderten Teil­bedingungen. Also entweder der Wert d1 ist kleiner als d2 oder die Werte sind gleich. Hier zeigt sich ganz besonders, dass logische Vergleiche nicht mit den gleich­lautenden Zahlenwert­vergleichen verwechselt werden dürfen.

Dieser Fehler in der Anwendung der Operatoren „<=“ und „>=“ auf Gleitkomma­werte wird meiner Erfahrung nach oft begangen. Selbst dann, wenn skalierte Epsilons in Vergleichen Anwendung finden. Das liegt auch sicherlich daran, dass die Materie nicht unmittelbar intuitiv ist und dass diese beiden Operatoren eine gesonderte Aufmerksam­keit erfordern. Sie dürfen nicht verwendet werden, um einen logischen Vergleich zwischen Fließ­komma­werten zu formulieren. Gleichwohl können sie angewendet werden, um die Zahlen­darstellung zu vergleichen, wie es beispiels­weise im logischen Vergleich auf Gleichheit geschieht.

Die Anwendung von Gleitkommawerten und Hilfestellung aus der Bibliothek

Wer das volle Potential der Fließkommawerte in digitalen Systemen nutzen möchte tut gut daran, ein Studium der höheren Mathematik mit Vertiefungs­richtung Numerik absolviert zu haben. Dann dürfte es vielleicht noch angesagt sein, den eigenen Code gegenüber jeder Änderung von fremder Hand standhaft zu verteidigen, wenn diese Hand nicht im Auftrag einer mindestens ebenso spezifisch akademisch gestählten und verfeinerten Persönlichkeit handelt.
Da ich mich selbst nicht zu diesem Personen­kreis zählen kann, besteht meine vorrangige Strategie der korrekten Verwendung von Gleitkomma­arithmetik in deren Vermeidung. Ich mache es nur wenn ich gezwungen werde!
Solange man etwas in eine ganzzahlige Darstellung bringen kann, sollte man es tun. Nur wenn die Rahmen­bedingungen die Nutzung von Gleitkomma­darstellung notwendig macht, wendet man sie an. Und dann mit einem möglichst tiefen Verständnis. Kommen wir nun zu einem sehr üblichen, meiner Erfahrung nach dem häufigsten Anwendungs­fall der Fließkomma­arithmetik:

Die Überprüfung von Messwerten anhand von Schwellenwerten

Was passiert, wenn Vergleiche anhand von Schwellen­werten durchgeführt werden? Im Allgemeinen kann gesagt werden, dass Werte auf eine bestimmte Maßeinheit umgerechnet werden, um sie schließlich vergleichen zu können. Um Werte auf eine Einheit zu normalisieren müssen sie gestreckt oder gestaucht und verschoben werden. Die Streckung oder Stauchung erfolgt durch eine Multiplikation oder Division, die Verschiebung durch eine Addition oder Substraktion. In den meisten Fällen der Messwert­bearbeitung kann also von der Erhebung des Wertes bis zum Test gegenüber dem Schwellenwert von zwei Rechen­operationen ausgegangen werden: von einer Punkt- und von einer Strichoperation. Glücklicherweise und höchst­wahr­scheinlich nicht ganz zufällig kennt der IEEE 754 eine solche Doppel­operation, bei der nur eine einfache Epsilon­abweichung an Ungenauigkeit anfällt. Man nennt sie die Fused Multiply Add-Operation, kurz FMA. In der C-Bibliothek wird ab C99 diese Doppel­operation in Form von Funktionen für die drei Fließkomma­datentypen float, double und long double angeboten:

float  fmaf( float x, float y, float z );
double  fma( double x, double y, double z );
long double  fmal( long double x, long double y, long double z );

Diese Funktionen berechnen (x × y) + z mit nur einer einzigen maximalen möglichen ULP Abweichung im Ergebnis. Sie garantieren, dass keine Fehler­fort­schreibung zwischen den beiden mathematischen Operationen stattfindet. Damit steht eine Lösung für ein weitverbreitetes Standard­problem zur Verfügung. Die Normierung von Messwerten kann in einem Arbeitsschritt erfolgen der aus zwei arithmetischen Operationen besteht aber nur eine Standard­ungenauigkeit verursacht. Die oben beschriebenen Verfahren der Gleitkomma­vergleiche können also direkt zur Anwendung gebracht werden.

Weitere Bibliotheksfunktionen

Im Abschnitt über die ULPs wurde die Distanz zwischen den darstellbaren Zahlen beschrieben. Mathematisch werden diese Distanzen mit skalieren Epsilons ausgedrückt, technisch sind es die ULPs, die Units in the Last Place. Die Distanz zwischen zwei darstell­baren Zahlen ist also ein ULP oder eben ein skaliertes Epsilon. Das bedeutet, dass es für den Umgang mit der Darstellungs­ungenauigkeit von Gleitkomma­werten neben der mathematischen Lösung über die Berechnung des Epsilons auch eine technische Lösung existieren kann, die mit den ULPs operiert.
Dazu gibt es ab dem C-Standard C99 eine kleine Sammlung von Funktionen in der Headerdatei <math.h>. Für C++ steht diese Funktions­sammlung ab dem Standard C++11 in der Headerdatei <math> zur Verfügung. Beginnen wir mit der folgenden Funktion, die stell­vertretend für die ganze Funktions­gruppe gesehen werden kann:

double  nextafter( double f /* from */, double t /* to */ );

Diese Funktion liefert die nächste darstellbare Zahl in der Nachbarschaft von f in die Richtung des Wertes von t.

#include <math.h>
#include <stdio.h>

int main()
{
    double a = nextafter( 0.0,  1.0 );
    double b = nextafter( 0.0, -1.0 );
    printf( "Die kleinste positive Zahl des Typs double: %E\n", a );
    printf( "Die kleinste negative Zahl des Typs double: %E\n", b );
    return 0;
}
Listing 6: C-Beispiel zur Anwendung der Funktion nextafter().

Das Beispiel in Listing 6 zeigt die An­wendung der Funk­tion. Sie liefert aus­gehend vom Wert des ersten Para­meters je nachdem, ob der zweite Para­meter kleiner oder größer als der erste Para­meter ist, den nächsten dar­stell­baren kleineren oder eben den nächsten dar­stell­baren größeren Wert.

Die anderen Funktionen in der kleinen Funktions­familie machen eigenlich genau das gleiche, sie arbeiten nur mit anderen Typen. Also auch mit float und long double. Es gibt die Funktionen mit den Namen nextafter..() und solche mit den Namen nexttoward..(). An der Stelle der beiden Punkte kann entweder gar nichts, ein „f“ für float oder ein „l“ für long double stehen.
Die Funktionen mit den Namen nexttoward..() nehmen als zweiten Parameter immer ein long double. Die Funktionen sind unter anderem in der C++ Referenz hinter diesem Link beschrieben. Sie sind auch anderstwo recht gut dokumentiert und wenn man einen der Namen kennt, findet man leicht ausreichende Dokumentation.

Setzt man diese Funktionen nun ein, um Gleitkomma­werte korrekt miteinander zu vergleichen, muss man natürlich die mögliche Anzahl der voraus­gegangenen Berechnungs­schritte mit in Betracht ziehen, genau so, wie man es auch mit einer auf das Eposilon basierten Bereichs­überprüfung machen würde. Wenn beide Werte errechnet sind, muss die doppelte ULP Distanz herangezogen werden.

Weitere Darstellungsformate

Erweiterte 80-Bit Formate

Erweiterte 80-Bit Fließkommaformate belegen 10 Bytes. Es gibt verschiede Versionen solcher Formate, die auch intern unterschiedlich aufgebaut sind. Hauptsächlich wurden sie von Chipherstellern entwickelt, die mit solchen breiteren Formaten die standardisierte doppelte Genauigkeit unterstützen. So führte Intel das x86 Extended-Precision Format erstmals für den mathematischen Coprozessor Intel 8087 ein.

Prozessorintern findet dieses Format weite Verbreitung. Der IEEE 754 Standard empfiehlt ein solches 80-Bit Format zur Unterstützung der Berechnungen im Standardformat der doppelten Genauigkeit. Er definiert jedoch nicht den internen Aufbau.

Das Quadrupel Format

Das Quadrupel Fließkommaformat belegt 16 Bytes im Speicher eines Computers und ist im IEEE 754 Standard standardisiert.

Der in C und C++ existierende Typ long double ist nicht nicht zwingend mit der vierfachen Genauigkeit definiert. Er kann doppelt genau wie double oder eben genauer sein. Aktuelle C++ Compiler – Januar 2026 – auf 64-Bit Systemen unterstützen jedoch häufig das Quadrupel Format mit long double. C Compiler bieten häufig den Typ __float128, der das IEEE standardisierte Quadrupel Formt unterstützt. Bei manchen Compilern ist long double und __float128 identisch. In diesem Fall ist der Typ long double auch der standardisierte Quadrupel Fließkommatyp. Im C++ Sprachstandard ist der Datentyp long double nicht fest definiert. Er muss nur breiter oder gleich double sein.

Denormalisierter Bereich
außer der 0
Normalisierter Bereich,
effektiver Darstellungsbereich
quadrupel precision ± { 2−16494 bis (1 − 2−112) × 2−16382} ± { 2−16382 bis (2 − 2−112) × 216383}

Die Logik der Darstellung funktioniert wie bei der doppelten Genauigkeit. Die Mantisse hat 112 Bits der Exponent 15 und der Bias ist 16383. Deshalb gibt es auch einen normalisierten und einen denormali­sierten Bereich. Die Ungenauigkeit wird durch ein Epsilon definiert, das man skalieren muss.

Links zum Thema Fließkommazahlen


Zuletzt geändert am 10.01.2026