Prüfe Fließkommazahlen nie auf Gleichheit!


Abstract

Probleme beim Vergleichen von Fließkommazahlen. Es ist ein beliebter Anfängerfehler, auch wenn ihn hin und wieder auch mal Programmierer machen, die es wissen sollten, weil sie eigentlich über das nötige Hintergrundwissen verfügen (sollten). Und zwar der Vergleich von Fließkommazahlen auf Gleichheit. Was ist daran so gefährlich und warum funktioniert es eben nicht immer?

Ein praktisches Beispiel

Gegeben sei folgendes kleines Demo-Programm:

#include 
#include 
#include 

void main() {
	float a = 69.82;
	float b = 69.2 + 0.62;
	bool bEqual = (a == b);

	printf("a: %.20f\n", a);
	printf("b: %.20f\n", b);

	if (bEqual)
		printf("gleich\n\n");
	else
		printf("ungleich\n\n");

Man sollte meinen, dass bEqual wahr ergibt. Tut es aber nicht. Betrachten wir mal die beiden Ausgaben vor dem Vergleich:

a: 69.81999999999999300000
b: 69.82000000000000700000

Offensichtlich unterscheiden sich die Zahlen, womit unser Programm völlig richtig entschieden hat, dass die Zahlen nicht gleich sind.

Begründung

Darstellung von Dezimalbrüchen im Dualsystem

Bekanntlich kennt der Computer nur Nullen und Einsen und rechnet somit auch im Dualsystem. Der Mensch ist allerdings an das Dezimal- oder auch Zehnersystem gewöhnt, weswegen die Eingaben üblicherweise auch in diesem Zahlensystem erfolgen. Damit der Computer nun etwas mit diesen Eingaben anfangen kann, muss er sie ins Dualsystem umrechnen.

Bei beiden Zahlensystem handelt es sich um ein Stellenwertsystem, dass heißt die Stelle, an der eine Ziffer des Zahlensystems steht, bestimmt ihre Wertigkeit. Steht die eins im Dezimalsystem an der ersten Stelle, hat sie die Wertigkeit eins; steht sie an der zweiten Stelle, hat sie die Wertigkeit zehn und so weiter:

10910810710610510410310210110010-110-210-310-4
00001234567890

ergibt im Dezimalsystem die Zahl:

1 * 105   =  100000
2 * 104   =   20000
3 * 103   =    3000
4 * 102   =     400
5 * 101   =      50
6 * 100   =       6
7 * 10-1  =       0,7
8 * 10-2  =       0,08
9 * 10-3  =       0,009
          => 123456,789

Umrechnen von Dezimalzahlen in das Dualsystem

Rechnen wir nun mal einen Dezimalbruch (19,3) in das Dualsystem um:

                         19
3
19 : 2 = 9 R 1
 9 : 2 = 4 R 1
 4 : 2 = 2 R 0
 2 : 2 = 1 R 0
 1 : 2 = 0 R 1


von unten nach oben gelesen:
                       10011
0,3 * 2 = 0,6 + 0
0,6 * 2 = 0,2 + 1
0,2 * 2 = 0,4 + 0
0,4 * 2 = 0,8 + 0
0,8 * 2 = 0,6 + 1
0,6 * 2 = 0,2 + 1

von oben nach unten gelesen:
010011

Die Kontrolle des ganzzahligen Anteils sparen wir uns mal und kontrollieren nur den Anteil hinter dem Komma:

2-1 (0,5)2-2 (0,25)2-3 (0,125)2-4 (0,0625)2-5 (0,03125)2-6 (0,015625)
010011

ergibt im Dezimalsystem:

0 * 0,5      =  0
1 * 0,25     =  0,25
0 * 0,125    =  0
0 * 0,0625   =  0
1 * 0,03125  =  0,03125
1 * 0,015625 =  0,015625
             => 0,296875

Was ist aus unserer 0,3 geworden? Wie man sieht offensichtlich 0,296875. Das Warum läßt sich auch ganz einfach erklären. Man hat mit der Wertigkeit der Stellen im Dualsystem nicht die Möglichkeit die Dezimalzahl 0,3 genau abzubilden. Und genau diese Diskrepanz ist der Grund, warum man Fließkommazahlen nie direkt vergleichen darf.

Die Lösung

Einführung einer Toleranz

Wie kann man aber trotzdem Fließkommazahlen auf Gleichheit prüfen? Man kann dies nur, in dem man eine Toleranz einbaut:

double dEpsilon = DBL_EPSILON * 100;
	
printf("dEpsilon: %.20f\n", dEpsilon);
printf("fabs(a - b): %.20f\n", fabs(a - b));
	
printf("%.20f < %.20f\n", fabs(a - b), dEpsilon);
bEqual = fabs(a - b) < dEpsilon;

if (bEqual)
	printf("gleich\n\n");
else
	printf("ungleich\n\n");

Hier wird bEqual tatsächlich wahr. Wie groß man die Toleranz wählt, hängt von den Werten ab, die man erwartet. Wählt man den Wert zu klein, kann der Vergleich auch wieder fehlschlagen, wählt man den Wert zu groß, werden Zahlen, die eigentlich unterschiedlich sind, als gleich betrachtet. Dies Verfahren ist also nicht unbedingt sicher, bietet aber einen vernünftigen Kompromiss.

Rechnen mit Ganzzahlen

Eine andere Möglichkeit besteht darin Fließkommazahlen zu meiden. Wenn man ausschließlich mit Zahlen zu tun hat, die nur wenige Nachkommastellen aufweisen und sich ausschließlich in dem durch Integer-Datentypen abbildbaren Wertebereichen bewegen, dann kann man durchaus Rechenschritte mit Integer-Operationen ausführen.

Reduziert man die Anzahl der signifikanten Stellen und benutzt statt dem Datentyp double (signifikate Stellen: 15 bis 16) den Datentyp float (signifikante Stelle: 7 bis 8), ergibt in diesem Beispiel auch schon der erste Vergleich, ohne Einführung einer Toleranz, wahr. Reduziert man in unserem Beispiel mit dem Dezimalbruch 19,3 die Genauigkeit auf eine Nachkommastelle, ergibt sich aus der 0,296875 auch wieder die 0,3.

Fazit

Wir haben also zwei entscheidende Faktoren: Einmal die Diskrepanz der Darstellung in einem anderen Zahlensystem und zum anderen die interne Darstellung / Genauigkeit von Fließkommazahlen. Diese beiden Faktoren sind dafür verantwortlich, dass ein Vergleich von Fließkommazahlen nie direkt erfolgen sollte.

Weiterführende Links

Links

[1] http://www.mpdvc.de/artikel/FloatingPoint.htm


2010-12-29T23:44:46 +0100, mail+homepage[at]michael-puff.de