Warum gibt Math.round (0.49999999999999994) 1 zurück?

567

Im folgenden Programm können Sie sehen, dass jeder Wert etwas kleiner als .5abgerundet ist, außer 0.5.

for (int i = 10; i >= 0; i--) {
    long l = Double.doubleToLongBits(i + 0.5);
    double x;
    do {
        x = Double.longBitsToDouble(l);
        System.out.println(x + " rounded is " + Math.round(x));
        l--;
    } while (Math.round(x) > i);
}

druckt

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 1
0.4999999999999999 rounded is 0

Ich verwende Java 6 Update 31.

Peter Lawrey
quelle
1
Auf Java 1.7.0 funktioniert es ok i.imgur.com/hZeqx.png
Kaffee
2
@Adel: Siehe meinen Kommentar zu Olis Antwort . Java 6 implementiert dies (und dokumentiert dies ) auf eine Weise, die zu einem weiteren Genauigkeitsverlust führen kann, indem 0.5die Zahl addiert und dann verwendet wird floor. Java 7 dokumentiert es nicht mehr so (vermutlich / hoffentlich, weil sie es behoben haben).
TJ Crowder
1
Es war ein Fehler in einem Testprogramm, das ich geschrieben habe. ;)
Peter Lawrey
1
Ein anderes Beispiel, das Gleitkommawerte zeigt, kann nicht zum Nennwert genommen werden.
Michaël Roy
1
Nachdem ich darüber nachgedacht habe. Ich sehe kein Problem. 0,49999999999999994 ist größer als die kleinste darstellbare Zahl kleiner als 0,5, und die Darstellung in dezimaler, vom Menschen lesbarer Form ist selbst eine Annäherung , die versucht, uns zu täuschen.
Michaël Roy

Antworten:

574

Zusammenfassung

In Java 6 (und vermutlich früher) round(x)ist als implementiert floor(x+0.5). 1 Dies ist ein Spezifikationsfehler für genau diesen einen pathologischen Fall. 2 Java 7 schreibt diese fehlerhafte Implementierung nicht mehr vor. 3

Das Problem

0,5 + 0,49999999999999994 ist genau 1 mit doppelter Genauigkeit:

static void print(double d) {
    System.out.printf("%016x\n", Double.doubleToLongBits(d));
}

public static void main(String args[]) {
    double a = 0.5;
    double b = 0.49999999999999994;

    print(a);      // 3fe0000000000000
    print(b);      // 3fdfffffffffffff
    print(a+b);    // 3ff0000000000000
    print(1.0);    // 3ff0000000000000
}

Dies liegt daran, dass 0,49999999999999994 einen kleineren Exponenten als 0,5 hat. Wenn sie also hinzugefügt werden, wird ihre Mantisse verschoben und der ULP wird größer.

Die Lösung

Seit Java 7 implementiert OpenJDK (zum Beispiel) es folgendermaßen: 4

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) // greatest double value less than 0.5
        return (long)floor(a + 0.5d);
    else
        return 0;
}

1. http://docs.oracle.com/javase/6/docs/api/java/lang/Math.html#round%28double%29

2. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6430675 (Dank an @SimonNickerson für das Auffinden dieses Dokuments)

3. http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round%28double%29

4. http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/java/lang/Math.java#Math.round%28double%29

Oliver Charlesworth
quelle
Ich sehe diese Definition nicht roundim Javadoc fürMath.round oder in der Übersicht der MathKlasse.
TJ Crowder
3
@ Oli: Oh, das ist interessant, sie haben das Bit für Java 7 (die Dokumente, mit denen ich verlinkt habe) herausgenommen - vielleicht, um zu vermeiden, dass diese Art von seltsamem Verhalten durch Auslösen eines (weiteren) Genauigkeitsverlusts verursacht wird.
TJ Crowder
@TJCrowder: Ja, das ist interessant. Wissen Sie, ob es für einzelne Java-Versionen Dokumente zu "Versionshinweisen" / "Verbesserungen" gibt, damit wir diese Annahme überprüfen können?
Oliver Charlesworth
6
@MohammadFadin: Schauen Sie sich zB en.wikipedia.org/wiki/Single_precision und en.wikipedia.org/wiki/Unit_in_the_last_place an .
Oliver Charlesworth
1
Ich kann nicht anders, als zu denken, dass dieses Update nur kosmetischer Natur ist, da Null am sichtbarsten ist. Es gibt zweifellos viele andere Gleitkommawerte, die von diesem Rundungsfehler betroffen sind.
Michaël Roy
83

Quellcode in JDK 6:

public static long round(double a) {
    return (long)Math.floor(a + 0.5d);
}

Quellcode in JDK 7:

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) {
        // a is not the greatest double value less than 0.5
        return (long)Math.floor(a + 0.5d);
    } else {
        return 0;
    }
}

Wenn der Wert in JDK 6 0,49999999999999994d ist, wird Floor aufgerufen und daher 1 zurückgegeben. In JDK 7 ifwird jedoch geprüft, ob die Zahl der größte Doppelwert unter 0,5 ist oder nicht. Da in diesem Fall die Zahl nicht der größte Doppelwert kleiner als 0,5 ist, gibt der elseBlock 0 zurück.

Sie können 0.49999999999999999d ausprobieren, was 1, aber nicht 0 zurückgibt, da dies der größte Doppelwert unter 0,5 ist.

Chandra Sekhar
quelle
Was passiert dann hier mit 1.499999999999999994? gibt 2 zurück? es sollte 1 zurückgeben, aber dies sollte Ihnen den gleichen Fehler wie zuvor bringen, aber mit einer 1.?
mmm
6
1.499999999999999994 kann nicht in Gleitkommazahlen mit doppelter Genauigkeit dargestellt werden. 1.4999999999999998 ist das kleinste Doppel kleiner als 1,5. Wie Sie der Frage floorentnehmen können, rundet die Methode sie korrekt ab.
OrangeDog
26

Ich habe das gleiche auf JDK 1.6 32-Bit, aber auf Java 7 64-Bit habe ich 0 für 0.49999999999999994, wobei gerundet 0 ist und die letzte Zeile nicht gedruckt wird. Es scheint ein VM-Problem zu sein. Bei Verwendung von Gleitkommazahlen sollten Sie jedoch erwarten, dass sich die Ergebnisse in verschiedenen Umgebungen (CPU-, 32- oder 64-Bit-Modus) geringfügig unterscheiden.

Und wenn roundMatrizen usw. verwendet oder invertiert werden, können diese Bits einen großen Unterschied machen.

x64-Ausgabe:

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 0
Donau-Seemann
quelle
In Java 7 (der Version, mit der Sie es testen) ist der Fehler behoben.
Iván Pérez
1
Ich denke du meintest 32 Bit. Ich bezweifle, dass en.wikipedia.org/wiki/ZEBRA_%28computer%29 Java ausführen kann, und ich bezweifle, dass es seitdem eine 33-Bit-Maschine gibt.
chx
@chx ganz offensichtlich, weil ich 32 Bit vor geschrieben habe :)
Danubian Sailor
11

Die Antwort im Folgenden ist ein Auszug aus einem Oracle- Fehlerbericht 6430675 unter. Besuchen Sie den Bericht für die vollständige Erklärung.

Die Methoden {Math, StrictMath.round sind operativ definiert als

(long)Math.floor(a + 0.5d)

für doppelte Argumente. Während diese Definition normalerweise wie erwartet funktioniert, ergibt sie für 0x1.fffffffffffffp-2 (0.49999999999999994) das überraschende Ergebnis 1 anstelle von 0.

Der Wert 0,49999999999999994 ist der größte Gleitkommawert unter 0,5. Als hexadezimales Gleitkomma-Literal ist sein Wert 0x1.fffffffffffffp-2, was gleich (2 - 2 ^ 52) * 2 ^ -2 ist. == (0,5 - 2 ^ 54). Daher der genaue Wert der Summe

(0.5 - 2^54) + 0.5

ist 1 - 2 ^ 54. Dies ist auf halbem Weg zwischen den beiden benachbarten Gleitkommazahlen (1 - 2 ^ 53) und 1. In der von Java verwendeten arithmetischen Rundung auf den nächsten geraden Rundungsmodus nach IEEE 754 ist der Wert der beiden, wenn ein Gleitkommaergebnis ungenau ist, umso näher Darstellbare Gleitkommawerte, die das genaue Ergebnis enthalten, müssen zurückgegeben werden. Wenn beide Werte gleich nahe beieinander liegen, wird derjenige zurückgegeben, dessen letztes Bit Null ist. In diesem Fall ist der korrekte Rückgabewert aus der Addition 1, nicht der größte Wert kleiner als 1.

Während die Methode wie definiert arbeitet, ist das Verhalten bei dieser Eingabe sehr überraschend. Die Spezifikation könnte dahingehend geändert werden, dass sie eher auf die nächste lange, runde Verbindung rundet, wodurch das Verhalten dieser Eingabe geändert werden kann.

shiv.mymail
quelle