Simon Reinhardt: Fließkommafehler

Hi Forum!

Java hat anscheinend extreme Fehler in der Fliesskommaberechnung.
Hier ein kleiner Auszug aus meinem Programm:

for (double x=section.from; x<=section.to; x+=section.step)
{
System.out.println(x);
calcedpoints.add(calcpoint(term, var, x));
}

Das Programm geht double-Werte im Bereich von section.from (-3) bis section.to (3) in der Schrittweite section.step (0.2) durch und berechnet dazu Werte.
Mit System.out.println lasse ich mir die x-Werte Testweise ausgeben und erhalte vollkommen Krumme Nachkommastellen:

-3.0
-2.8
-2.5999999999999996
-2.3999999999999995
-2.1999999999999993
-1.9999999999999993
-1.7999999999999994
-1.5999999999999994
-1.3999999999999995
-1.1999999999999995
-0.9999999999999996
-0.7999999999999996
-0.5999999999999996
-0.39999999999999963
-0.19999999999999962
3.885780586188048E-16
0.2000000000000004
0.4000000000000004
0.6000000000000004
0.8000000000000005
1.0000000000000004
1.2000000000000004
1.4000000000000004
1.6000000000000003
1.8000000000000003
2.0000000000000004
2.2000000000000006
2.400000000000001
2.600000000000001
2.800000000000001

Wie ihr seht, stimmen nur ersten beiden Werte.
Anscheinend hat JavaScript das gleiche Problem, da dies hier schon mal beschrieben wurde. Folgender Tip wurde damals gegeben:

var zahl1 *= 100;
var zahl2 *= 100;

var zahl3 = zahl1 + zahl2;

zahl3 /= 100;

Auf mein Problem angewandt ergibt das folgenden Code:

double x=section.from;
while(x<=section.to)
{
System.out.println(x);
calcedpoints.add(calcpoint(term, var, x));
x*=100;
x+=section.step*100;
x/=100;
}

Aber auch das gibt keine wirklich zufriedenstellenden Ergebnisse:

-3.0
-2.8
-2.6
-2.4
-2.2
-2.0000000000000004
-1.8000000000000005
-1.6000000000000005
-1.4000000000000006
-1.2000000000000006
-1.0000000000000007
-0.8000000000000007
-0.6000000000000008
-0.4000000000000008
-0.2000000000000008
-7.815970093361102E-16
0.1999999999999992
0.39999999999999925
0.5999999999999992
0.7999999999999992
0.9999999999999991
1.199999999999999
1.3999999999999988
1.5999999999999988
1.799999999999999
1.999999999999999
2.199999999999999
2.399999999999999
2.5999999999999988
2.799999999999999
2.9999999999999987

(Vielen Dank an alle, die bis hierhin tapfer durchgehalten haben ;-) )

Fuer mein Programm ist es unabdingbar, vollkommen exakte Werte zu erhalten. Da es auch vorkommen kann, dass sich section in Bereichen wie -0.00000001 bis 0.00000001 bewegt, hilft auch kein Runden.
Wie kann ich Java dazu bringen, richtig zu rechnen?
Warum kann Sun noch nicht mal die Addition von Fliesskommawerten exakt implementieren?

Bitte helft mir, ich bin total verzweifelt...

MfG Simon

  1. Hallo

    ich habe mir Deinen Text nicht von vorne bis hinten angetan. Mir ist aber das Problem mit rechnen von Fließkommazahlen im Allgemeinen bekannt. Das liegt üblicherweise an der Darstellung derZahlen im Computer, welche ja binär und nicht mit dem "normalen" Zahlensystem, erfolgt. Dadurch kommt es zu solchen Fehlern, weil 64 Bit Zahlen (oder wasauch Double ist) bei mehreren Durchläufen nicht mehr genau genug ist. Das ist übrigens kein Java-spezifisches Problem, sondern betrifft alle Programmiersprachen. Da hilft dann wirklich nur an geeigneter Stelle geschickt runden, um die Fehler auszugleichen.

    Gruß
       Michael

  2. Hi,

    Michal hat Deine Frage im Wesentlichen beantwortet. Stell Dir doch mal die Frage, warum Du den Bereich ausgerechnet in 0.2er Schritten durchgehst. Ganz einfach: weil Du gewohnt bist, im Dezimalsystem zu rechnen und 0.2er Schritte dort schöne runde Zahlen ergeben. Du würdest nie auf die Idee kommen, etwa 1/7-Schritte zu verwenden, weil schon der erste Schritt mit einer Dezimalzahl nicht exakt dargestellt werden kann, und nach sieben Mal Addieren eben nicht exakt 1.0 herauskommt - egal, mit wieviel Stellen Genauigkeit man rechnet. Genau das gleiche Problem hat der Computer, wenn er mit 0.2 rechnen soll. Was Du ihm als "extreme Fehler" ankreidest, sind zwangsläufige Abweichungen in der letzten Kommastelle. Der Computer kann z.B. den Wert 2.6 nicht exakt berechnen, weil es für diesen Wert gar keine Darstellung im Binärsystem gibt. Das Gleiche gilt auch für 2.8, nur sind nach der ersten Rechnung die Abweichungen noch so gering, dass sie weggerundet werden.

    Der Trick mit dem *100 und späteren /100 kann in sofern funktionieren, als dabei die meiste Zeit mit ganzen Zahlen (also exakt!) gerechnet wird und nur ganz zuletzt eine einzelne unexakte Operation folgt. Du hast den Trick allerdings falsch angewendet, da bei Dir doch wieder in jedem Schritt eine unexakte Operation erfolgt (/100), die in alle folgenden Schritte einfließt. Du müsstest vollständig in ganzen Zahlen rechnen und erst das Ergebnis unmittelbar vor der Ausgabe durch 100 (oder was auch immer) teilen.

    Es gibt m.E. nur zwei wirklich saubere Lösungen für das Problem:

    (1) Du lässt dem Rechner die Hoheit über seine Zahlen, musst dann aber die standardmäßige Umwandlung Zahl->String algorithmisch nachbearbeiten, da diese von einer zu hohen Genauigkeit ausgeht. Ein Wert wie z.B. "-1.2000000000000006" müsste dann über String-Funktionen (nicht numerisch!) auf "-1.2" gebracht werden.

    (2) Du rechnest von vornherein nur mit Zahlen, die im Binärsystem exakt darstellbar sind. Statt 0.2er Schritten könntest Du z.B. 0.25er oder 0.203125er (=13/64) Schritte wählen.

    Ach ja: Ich kann kein Java, das sind alles allgemeine Aussagen, die aber auch für Java zutreffen müssten.

    Gruß
    Steffen

    1. Hi Steffen!

      Vielen Dank fuer die ausfuehrliche Erklaerung, ich glaub, ich hab's jetzt.

      (2) Du rechnest von vornherein nur mit Zahlen, die im Binärsystem exakt darstellbar sind. Statt 0.2er Schritten könntest Du z.B. 0.25er oder 0.203125er (=13/64) Schritte wählen.

      Kann ich mir leider nicht aussuchen, da die Schrittweite vom Benutzer eingegeben wird und so halt nicht kontrollierbar ist. 0.2 habe ich nur fuer Testzwecke benutzt.

      (1) Du lässt dem Rechner die Hoheit über seine Zahlen, musst dann aber die standardmäßige Umwandlung Zahl->String algorithmisch nachbearbeiten, da diese von einer zu hohen Genauigkeit ausgeht. Ein Wert wie z.B. "-1.2000000000000006" müsste dann über String-Funktionen (nicht numerisch!) auf "-1.2" gebracht werden.

      Ueber Strings? Das ist mir nicht effektiv genug. Aber es muesste eigentlich funktionieren, wenn ich die Nachkommastellen der Schrittweite (in diesem Fall also eine) zaehle und dann jeweils auf soviele Stellen runde.

      MfG Simon

      1. Ueber Strings? Das ist mir nicht effektiv genug.

        Es geht nur um die Ausgabe, nicht um die Berechnung! Die standardmäßige Ausgabe rundet zu genau, das ist das ganze Problem. Wenn es ein Ausgabeformat mit Angabe der Anzahl von Stellen gibt, wäre damit das Problem schon gelöst. Andernfalls müsste man halt selbst etwas derartiges programmieren, aber es kann eben nur am Punkt der Umwandlung Zahl->String ansetzen.

        Vielleicht beschreibst Du mal die komplette Problemstellung. In aller Regel müssen Gleitkommazahlen gar nicht 100% genau sein. Bestimmt lässt sich auch für Deinen Fall eine einfache und passende Lösung finden.

        Was wäre z.B., wenn Du nicht den Gleitkommawert x incrementierst, sondern eine Ganzzahl i und x daraus berechnest:

        for (i=0; i*section.step<(section.to-section.from); i++)
        {
        x=section.from+i*(section.to-section.from)/section.step;
        ...
        }

        Auch das ist nicht 100% genau (wie gesagt, kann es nicht), aber die Ungenauigkeiten addieren sich nicht mehr mit jedem Schleifendurchlauf. Bestimmt reicht diese Genauigkeit, um Werte anzuzeigen, die 100% genau aussehen.

        Gruß
        Steffen

        P.S.: Mit dem Simon Reinhardt von http://www.pics-software.de/start.htm bist Du nicht identisch, oder?

        1. Hi!

          Vielleicht beschreibst Du mal die komplette Problemstellung. In aller Regel müssen Gleitkommazahlen gar nicht 100% genau sein. Bestimmt lässt sich auch für Deinen Fall eine einfache und passende Lösung finden.
          Auch das ist nicht 100% genau (wie gesagt, kann es nicht), aber die Ungenauigkeiten addieren sich nicht mehr mit jedem Schleifendurchlauf. Bestimmt reicht diese Genauigkeit, um Werte anzuzeigen, die 100% genau aussehen.

          Also gut: das ganze gibt einen Funktionsplotter. Man gibt eine beliebige Funktion und den dazugehoerigen Berechnungsintervall ein und mein Programm berechnet diese Funktion in dem angegebenen Intervall und zeichnet sie in ein Koordinatensystem.
          Fuer die Funktion selbst ist es vielleicht nicht so wichtig, 100%tige Ergebnisse zu erhalten, auch wenn die Punkte spaeter mit ausgegeben werden.
          Aber auch die Achsenbeschriftung laeuft ueber so einen Intervalldurchlauf.
          Und wenn dann an der Achse

          0, 0.1, 0.2, 0.2999999999, ...

          steht, sieht das schon ein bisschen doof aus. Aber ich denke, ich krieg das schon irgendwie mit dem Runden und der Ausgabe hin - wenn ich irgendwo haenge, melde ich mich nochmal :-)

          P.S.: Mit dem Simon Reinhardt von http://www.pics-software.de/start.htm bist Du nicht identisch, oder?

          *g* Nein, aber ich kenn den schon (hat mir mal ne mail geschickt)

          MfG Simon

          1. Hi nochmal!

            Ich glaub, ich hab's jetzt hingekriegt.
            Ich hab noch zwei Funktionen geschrieben, die das Stellenzaehlen und das Runden auf eine bestimmte Stellenanzahl uebernehmen:

            public static int floatingdigits(double d)
            {
            String s = d+"";
            int pos = s.indexOf(".");
            if(pos != 0 && pos != s.length()-1)
              return s.substring(pos+1, s.length()).length();
            return 0;
            }

            public static double roundtodigits(double d, int digits)
            {
            d *= Math.pow(10, digits);
            return Math.rint(d) / Math.pow(10, digits);
            }

            Fuer die Schleife sieht das dann so aus:

            int digits = floatingdigits(section.step);
            for (double x=section.from; x<=section.to; x=roundtodigits(x+section.step, digits))
            calcedpoints.add(calcpoint(term, var, x));

            Ich zaehle die Stellen der Schrittweite und runde damit den Zaehler x. So erhalte ich genau die richtigen Werte.
            Lustig wird's allerdings, wenn ich die Schrittweite verfeinere: bei 0.0001 rechnet er noch lustig vor sich hin, auch wenn es ein bisschen laenger dauert. Bei 0.00001 erhoeht er x allerdings gar nicht mehr, sondern zeigt nur noch -3 an. Naja, ich glaube aber nicht, dass jemand eine Funktion so genau berechnet haben will, hier kann ich also getrost eine Beschraenkung einbauen :-)

            MfG Simon