Почему Math.round(0.49999999999999994) возвращает 1?

В следующей программе вы увидите, что каждое значение чуть меньше .5 округляется вниз, за ​​исключением 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);
}

печатает

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

Я использую обновление Java 6.

Ответ 1

Резюме

В Java 6 (и предположительно ранее) round(x) реализуется как floor(x+0.5). 1 Это ошибка спецификации для именно этого одного патологического случая. 2 Java 7 больше не требует этой сломанной реализации. 3

Проблема

0,5 + 0,49999999999999994 - ровно 1 с двойной точностью:

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
}

Это потому, что 0.49999999999999994 имеет меньшую экспоненту, чем 0,5, поэтому, когда они добавляются, ее мантисса сдвигается, и ULP становится больше.

Решение

Так как Java 7, OpenJDK (например) реализует его таким образом: 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 (кредиты @SimonNickerson за то, что вы нашли это) Суб >

<суб > 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 Суб >

Ответ 3

Исходный код в JDK 6:

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

Исходный код в 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;
    }
}

Когда значение равно 0.49999999999999994d, в JDK 6 он вызывается floor и, следовательно, возвращает 1, но в JDK 7 условие if проверяет, является ли это число наибольшим двойным значением менее 0,5 или нет. Так как в этом случае число не является наибольшим двойным значением менее 0,5, поэтому блок else возвращает 0.

Вы можете попробовать 0.49999999999999999d, который вернет 1, но не 0, потому что это самое большое двойное значение меньше 0,5.

Ответ 4

У меня то же самое на JDK 1.6 32-bit, но на Java 7 64-bit у меня есть 0 для 0.49999999999999994, который округлен 0, а последняя строка не печатается. Однако, похоже, проблема с VM, используя плавающие точки, вы должны ожидать, что результаты будут немного отличаться в разных средах (CPU, 32- или 64-разрядный режим).

И при использовании round или инвертирующих матриц и т.д. эти биты могут иметь огромное значение.

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

Ответ 5

В дальнейшем мы получим отрывок из Oracle сообщение об ошибке 6430675 at. Посетите отчет для полного объяснения.

Методы {Math, StrictMath.round оперативно определены как

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

для двойных аргументов. Хотя это определение обычно работает так, как ожидалось, оно дает неожиданный результат 1, а не 0, для 0x1.fffffffffffffp-2 (0.49999999999999994).

Значение 0.49999999999999994 является наибольшим значением с плавающей запятой менее 0,5. В качестве шестнадцатеричного литерала с плавающей запятой его значение равно 0x1.fffffffffffffp-2, что равно (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Поэтому точное значение суммы

(0.5 - 2^54) + 0.5

составляет 1 - 2 ^ 54. Это на полпути между двумя соседними числами с плавающей запятой (1 - 2 ^ 53) и 1. В арифметическом раунде IEEE 754 до ближайшего режима округления, используемого Java, когда результаты с плавающей запятой неточны, представляемые значения с плавающей запятой, которые скопируют точный результат; если оба значения одинаково близки, тот, который возвращается его последний бит-ноль. В этом случае правильное возвращаемое значение из добавления равно 1, а не наибольшее значение меньше 1.

В то время как метод работает как определено, поведение на этом входе очень удивительно; спецификация может быть изменена на нечто большее, чем "Круглые до ближайших длинных округлых связей", что позволило бы изменить поведение на этом входе.