Dlaczego Math.round (0.4999999999999999994) zwraca 1?

567

W poniższym programie widać, że każda wartość jest nieco mniejsza niż .5zaokrąglona w dół, z wyjątkiem 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);
}

odciski

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

Używam aktualizacji Java 6 31.

Peter Lawrey
źródło
1
Na java 1.7.0 działa ok i.imgur.com/hZeqx.png
Kawa
2
@Adel: Zobacz mój komentarz do odpowiedzi Oli , wygląda na to , że Java 6 implementuje to (i dokumenty, które robi ) w sposób, który może spowodować dalszą utratę precyzji poprzez dodanie 0.5do liczby, a następnie użycie floor; Java 7 nie dokumentuje go już w ten sposób (prawdopodobnie / mam nadzieję, że to naprawiły).
TJ Crowder
1
To był błąd w napisanym przeze mnie programie testowym. ;)
Peter Lawrey
1
Innego przykładu pokazującego wartości zmiennoprzecinkowe nie można przyjmować według wartości nominalnej.
Michaël Roy,
1
Po przemyśleniu. Nie widzę problemu. 0,4999999999999999994 jest większy niż najmniejsza reprezentowalna liczba mniejsza niż 0,5, a sama reprezentacja w postaci dziesiętnej czytelnej dla człowieka jest przybliżeniem, które próbuje nas oszukać.
Michaël Roy,

Odpowiedzi:

574

Podsumowanie

W Javie 6 (i prawdopodobnie wcześniej) round(x)jest implementowany jako floor(x+0.5). 1 To jest błąd specyfikacji, dokładnie dla tego jednego przypadku patologicznego. 2 Java 7 nie nakazuje już tej zepsutej implementacji. 3)

Problem

0,5 + 0,49999999999999994 to dokładnie 1 z podwójną precyzją:

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
}

Wynika to z faktu, że 0,4999999999999999994 ma wykładnik mniejszy niż 0,5, więc gdy zostaną dodane, jego mantysa jest przesuwana, a ULP staje się większy.

Rozwiązanie

Od wersji Java 7 OpenJDK (na przykład) implementuje to w ten sposób: 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 (podziękowania dla @SimonNickerson za znalezienie tego)

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
źródło
Nie widzę tej definicji roundw Javadoc dlaMath.round ani w przeglądzie Mathklasy.
TJ Crowder
3
@ Oli: Och, to interesujące. Wyjęli to z Java 7 (dokumenty, z którymi się połączyłem) - może w celu uniknięcia wywoływania tego rodzaju dziwnych zachowań poprzez (dalszą) utratę precyzji.
TJ Crowder
@TJCrowder: Tak, to interesujące. Czy wiesz, czy istnieją jakieś dokumenty „informacje o wydaniu” / „ulepszenia” dla poszczególnych wersji Java, abyśmy mogli zweryfikować to założenie?
Oliver Charlesworth
6
@MohammadFadin: spójrz na np. En.wikipedia.org/wiki/Single_precision i en.wikipedia.org/wiki/Unit_in_the_last_place .
Oliver Charlesworth,
1
Nie mogę nie myśleć, że ta poprawka jest tylko kosmetyczna, ponieważ zero jest najbardziej widoczne. Nie ma wątpliwości, że wiele innych wartości zmiennoprzecinkowych jest dotkniętych tym błędem zaokrąglenia.
Michaël Roy
83

Kod źródłowy w JDK 6:

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

Kod źródłowy w 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;
    }
}

Gdy wartość wynosi 0,49999999999999994d, w JDK 6 wywoła floor, a zatem zwróci 1, ale w JDK 7 ifwarunek sprawdza, czy liczba jest największą podwójną wartością mniejszą niż 0,5, czy nie. Podobnie jak w tym przypadku liczba nie jest największą podwójną wartością mniejszą niż 0,5, więc elseblok zwraca 0.

Możesz wypróbować 0,49999999999999999d, co zwróci 1, ale nie 0, ponieważ jest to największa podwójna wartość mniejsza niż 0,5.

Chandra Sekhar
źródło
co dzieje się z 1.499999999999999994 tutaj? zwraca 2? powinien zwrócić 1, ale to powinno dać ci taki sam błąd jak wcześniej, ale z 1.?
mmm
6
1.49999999999999999994 nie może być reprezentowany w zmiennoprzecinkowym podwójnej precyzji. 1.499999999999999998 to najmniejszy podwójny mniej niż 1,5. Jak widać z pytania, floormetoda zaokrągla je poprawnie.
OrangeDog,
26

Mam to samo na 32-bitowym JDK 1.6, ale na 64-bitowym Java 7 mam 0 dla 0,49999999999999994, którego zaokrąglenie wynosi 0 i ostatni wiersz nie jest drukowany. Wydaje się, że jest to problem z maszyną wirtualną, jednak używając liczb zmiennoprzecinkowych, należy oczekiwać, że wyniki będą się nieco różnić w różnych środowiskach (procesor, tryb 32- lub 64-bitowy).

A podczas używania roundlub odwracania macierzy itp. Te bity mogą mieć ogromną różnicę.

wyjście x64:

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
Żeglarz naddunajski
źródło
W Javie 7 (wersja, której używasz do testowania) błąd został naprawiony.
Iván Pérez
1
Myślę, że miałeś na myśli 32-bit. Wątpię, by en.wikipedia.org/wiki/ZEBRA_%28computer%29 mógł uruchomić Javę i wątpię, że od tego czasu istnieje 33-bitowa maszyna.
chx
@chx całkiem oczywiste, ponieważ napisałem już 32 bity :)
Danubian Sailor
11

Odpowiedź poniżej stanowi fragment raportu o błędach Oracle 6430675 pod adresem. Odwiedź raport, aby uzyskać pełne wyjaśnienie.

Metody {Math, StrictMath.round są zdefiniowane operacyjnie jako

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

dla podwójnych argumentów. Chociaż ta definicja zwykle działa zgodnie z oczekiwaniami, daje zaskakujący wynik 1 zamiast 0 dla 0x1.fffffffffffffp-2 (0,49999999999999994).

Wartość 0,49999999999999994 jest największą wartością zmiennoprzecinkową mniejszą niż 0,5. Jako szesnastkowy literał zmiennoprzecinkowy jego wartość wynosi 0x1.fffffffffffffp-2, co jest równe (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Dlatego dokładna wartość sumy

(0.5 - 2^54) + 0.5

wynosi 1 - 2 ^ 54. Znajduje się to w połowie odległości między dwiema sąsiednimi liczbami zmiennoprzecinkowymi (1–2 ^ 53) i 1. W rundzie arytmetycznej IEEE 754 do najbliższego parzystego trybu zaokrąglania używanego przez Javę, gdy wyniki zmiennoprzecinkowe są niedokładne, im bliżej dwóch reprezentatywne wartości zmiennoprzecinkowe, które zawierają dokładny wynik, muszą zostać zwrócone; jeśli obie wartości są jednakowo bliskie, zwracana jest ta, której ostatni bit zero. W takim przypadku poprawna wartość zwracana z dodania wynosi 1, a nie największa wartość mniejsza niż 1.

Podczas gdy metoda działa zgodnie z definicją, zachowanie na tych danych wejściowych jest bardzo zaskakujące; specyfikacja mogłaby zostać zmieniona na coś bardziej jak „Zaokrąglaj do najbliższego długiego, zaokrąglając więzi w górę”, co pozwoliłoby na zmianę zachowania na tym wejściu.

shiv.mymail
źródło