Das Repräsentationsphänomen
Betrachten wir folgenden Code:
C#-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.
Detailliertere Informationen findet man hier:
(deutsch)
Rundungsfehler beim Umwandeln von Fließkommawerten in Ganzzahlwerte
(englisch)
Five Tips for Floating Point Programming
Das Phänomen der Rundungsfehler
Betrachten wir folgenden Code:
C#-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?
C#-Code: |
if (Math.Abs(floatzahl1 - floatzahl2) < EPSILON)
|
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?
C#-Code: |
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.
C#-Code: |
Decimal d1 = 1;
Decimal d2 = d1 / 3m * 3m;
|
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.
Siehe auch
Gleichheit von Gleitkommazahlen (inkl. etwas Theorie)