Laden...
FAQ

[FAQ] Double und Float: Fehler beim Vergleich und Rundungsfehler

Erstellt von gelöschtem Konto vor 15 Jahren Letzter Beitrag vor 15 Jahren 38.163 Views
Gelöschter Account
vor 15 Jahren
[FAQ] Double und Float: Fehler beim Vergleich und Rundungsfehler

Einordnung

Die von C#/.NET verwendeten Gleitkommatypen float/Single und double/Double sind über CPU-Befehle realisiert und nach IEEE 754 genormt. Diese Norm wird von den meisten CPUs und in der Folge von den meisten Programmiersprachen verwendet. Daher ist das Folgende kein spezifisches Problem von C#/.NET, sondern ein Problem des weit verbreiteten Standards bzw. genereller gesprochen der auf Computern zwangsläufig begrenzten Genauigkeit der Zahlendarstellung.

Das Repräsentationsphänomen

Betrachten wir folgenden Code:

            
            double d2 = 0.1F;
            Console.WriteLine(d2);
            

Nun sollte die Ausgabe 0.1 sein. Wenn man nun aber den Code ausführt, so erhält man "0,100000001490112" als Ergebnis. Warum?
Das Problem ist, dass Gleitkommazahlen (float/double) eine endliche Genauigkeit haben. Binär betrachtet ergibt sich bei der Repräsentation von 0.1 eine Periodizität ( binär: 0,00011001100110011...), die aufgrund der Genauigkeit (32bit/64bit/80bit...) abgeschnitten wird. Beim Rückrechnen in das Dezimalsystem (für die Anzeige z.B.) entstehen somit Ungenauigkeiten.

Die begrenzte Genauigkeit der Repräsentation betrifft nicht nur Nachkommastellen, sondern kann auch Auswirkungen auf Vorkommastellen haben. So ist 16777216 die größte in einer Variable vom Typ float gerade noch exakt darstellbare ganze Zahl. 16777217 wird bereits auf 16777218 gerundet. Bei ganzen Zahlen mit noch mehr Stellen fallen die letzten Vorkommastellen komplett unter den Tisch. Float hat eine Genauigkeit von 7-8 Stellen. Bei double beträgt die Genauigkeit 15-16 Stellen. Und bei der u.g. Alternative decimal immerhin 28-29 Stellen.

Detailliertere Informationen findet man hier:
(deutsch)Rundungsfehler beim Umwandeln von Fließkommawerten in Ganzzahlwerte
(englisch)Five Tips for Floating Point Programming
(englisch)Why does a float variable stop incrementing at 16777216 in C#?

Das Phänomen der Rundungsfehler

Betrachten wir folgenden Code:


            float f = 1.11f;
            double d = 111f;

            for(int i = 0; i < 1000; i++)
            {
                float erg = ((float) d/100);
                if (erg != f)
                {
                    Console.WriteLine("fehler!");
                }
                    f = 1.11f;
                    d = 111f;                
            }
            Console.WriteLine("Fertig");
            Console.ReadLine();

Generiert man einen Debug-Stand und lässt ihn laufen, dann tritt keinerlei Fehler auf. Im Release-Stand allerdings tritt ein Fehler auf, aber auch nicht auf allen Rechnern und auch nur, wenn man nicht direkt aus VS startet (also Optimierungen zulässt).
Das ist eine Art von Fehlerquelle, die den meisten Programmierern nicht bewusst ist.

Die Ursache liegt in der Art und Weise, wie Gleitkommazahlen auf der CPU behandelt werden und was der JIT daraus macht. Wenn der Code im Debugmode ist, dann sind dem JIT jegliche CPU Optimierungen untersagt. Selbiges passiert auch, wenn sich ein Debugger anhängt. Im Release-Stand allerdings, wenn kein Debugger angehängt ist, versucht der JIT, CPU-spezifische Optimierungen am Ausführungscode vorzunehmen, um die Performance zu steigern.

Im obigen Code liegt die Ursache darin, das der JIT im Debug-Mode veranlasst, das die Zahlen vor dem Vergleich gerundet werden. Das geschieht in der CPU, wenn man die Zahl aus dem FPU-Stack herausnimmt. (Für genauere Informationen siehe obigen ersten Link und dort relativ am Ende des Beitrags.)

Wie vergleicht man nun Gleitkommadatentypen?


if (Math.Abs(floatzahl1 - floatzahl2) < EPSILON)
//EPSILON = 0.009

Hier nimmt man aber geflissentlich eine Ungenauigkeit in Kauf (das Epsilon). Ein absolut sicheres Vergleichen (also absolut genaues) ist jedoch mit Gleitkommatypen (ohne zu runden) nicht möglich.
In manchen Fällen (bei größeren Berechnung z.B.), ist es notwendig, die Ungenauigkeit (Toleranz) des Vergleiches zu erhöhen (z.B. 2 * Epsilon), da praktisch gesehen jede Operation mit Gleitkommadatentypen die Ungenauigkeit verschlimmert (Ungenau + Ungenau = doppelt so Ungenau).

Auch bei "<" und ">" Vergleichen muss man diese Ungenauigkeit in Kauf nehmen und im Hinterkopf behalten, dass unter Umständen die Bedingung zutrifft oder nicht zutrifft, obwohl sie es sollte.

Allgemein sollte Epsilon (also die Toleranz) so klein wie möglich und zugleich so groß wie nötig gehalten werden.

Prinzipiell sollte man daher Vergleiche von Gleitkommatypen wenn möglich vermeiden.

Wie groß soll Epsilon konkret sein?

        public static double Epsilon()
		{
			double tau = 1.0;
			double alt = 1.0;
			double neu = 0.0;

			while (neu != alt)
			{
				tau *= 0.5;
				neu = alt + tau;
			}

			return 2.0 * tau;
		}

Quelle

Es empfiehlt sich, diesen Wert beim Start eines Programms nur einmal berechnen zu lassen (sofern notwendig) und das Ergebnis in einer statischen Variable zu halten.

Siehe auch: float, double Arithmetik: Epsilon berechnen
_

Die Alternative

Eine Alternative zu float oder double ist Decimal (96bit).

Decimal ist primär für finanzmathematische Berechnungen gedacht, wo Rundungsfehler äußerst unangenehm und sehr kritisch sind.

Bei Addition und Subtraktion innerhalb des Genauigkeitsbereiches (28 Stellen nach dem Komma), treten hierbei keine Ungenauigkeiten auf. Lediglich bei Multiplikation mit gebrochenen Zahlen und Division, muss man unter Umständen bei einer daraus resultierenden Periodizität eine Rundung durchführen, wobei hier das Epsilon sehr klein gehalten werden kann, da die Periode über der Genauigkeit einfach abgeschnitten wird.

            Decimal d1 = 1;
            Decimal d2 = d1 / 3m * 3m; // ergibt 0.999999999999999999999....

Abschließendes: Der VB.NET Compiler baut automatisch zu Lasten der Performance Rundungen ein. In C# und C++ muss sich der Programmierer um das Runden selbst kümmern.

Hinweis
Microsoft empfiehlt ausdrücklich nicht die Verwendung von System.Double.Epsilon!

Because Epsilon defines the minimum expression of a positive value whose range is near zero, the margin of difference between two similar values must be greater than Epsilon. Typically, it is many times greater than Epsilon. Because of this, we recommend that you do not use Epsilon when comparing Double values for equality.

Siehe auch

Gleichheit von Gleitkommazahlen (inkl. etwas Theorie)