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

567

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

Питер Лори
источник
1
На Java 1.7.0 все работает нормально i.imgur.com/hZeqx.png
Coffee
2
@ Адель: См. Мой комментарий к ответу Оли , похоже, что Java 6 реализует это (и документирует это ) таким образом, что может вызвать дальнейшую потерю точности, добавляя 0.5к числу и затем используя floor; Java 7 больше не документирует это таким образом (вероятно / надеюсь, потому что они исправили это).
TJ Crowder
1
Это была ошибка в тестовой программе, которую я написал. ;)
Питер Лори
1
Другой пример, который показывает значения с плавающей запятой, не может быть взят по номиналу.
Микаэль Рой
1
Подумав об этом. Я не вижу проблемы. 0.49999999999999994 больше наименьшего представимого числа меньше 0,5, и представление в десятичной читаемой человеком форме само по себе является приближением , которое пытается нас обмануть.
Микаэль Рой

Ответы:

574

Резюме

В 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

Оливер Чарльзуорт
источник
Я не вижу этого определения roundв Javadoc дляMath.round или в обзоре Mathкласса.
TJ Crowder
3
@ Оли: О, это интересно, они взяли этот бит для Java 7 (документы, на которые я ссылался) - возможно, чтобы избежать такого странного поведения, вызвав (дальнейшую) потерю точности.
TJ Crowder
@TJCrowder: Да, это интересно. Знаете ли вы, есть ли какой-нибудь документ «Примечания к выпуску» / «Улучшения» для отдельных версий Java, чтобы мы могли проверить это предположение?
Оливер Чарльзуорт
6
@MohammadFadin: взгляните, например, на en.wikipedia.org/wiki/Single_precision и en.wikipedia.org/wiki/Unit_in_the_last_place .
Оливер Чарльзуорт
1
Я не могу не думать, что это исправление только косметическое, так как ноль наиболее заметен. Нет сомнений в том, что эта ошибка округления влияет на многие другие значения с плавающей запятой.
Микаэль Рой
232

Похоже, это известная ошибка (ошибка Java 6430675: Math.round имеет удивительное поведение для 0x1.fffffffffffffp-2 ), которая была исправлена ​​в Java 7.

Саймон Никерсон
источник
5
+1: хорошая находка! Связано с различиями в документации между Java 6 и 7, как объяснено в моем ответе.
Оливер Чарльзуорт
1
Реализация round () намного сложнее, чем многие думают
Шафик Ягмур
83

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

Чандра Сехар
источник
что тогда происходит с 1.499999999999999994? возвращает 2? он должен вернуть 1, но при этом вы получите ту же ошибку, что и ранее, но с 1.?
ммм
6
1.499999999999999994 не может быть представлен с плавающей точкой двойной точности. 1.4999999999999998 - это наименьшее двойное число меньше 1,5. Как видно из вопроса, floorметод округляет его правильно.
OrangeDog
26

У меня то же самое на 32-битной JDK 1.6, но на 64-битной Java 7 у меня 0 для 0.49999999999999994, округленное 0, и последняя строка не печатается. Кажется, это проблема виртуальной машины, однако при использовании чисел с плавающей запятой следует ожидать, что результаты будут немного отличаться в разных средах (процессор, 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
Дунайский моряк
источник
В Java 7 (версия, которую вы используете для тестирования) ошибка исправлена.
Иван Перес
1
Я думаю, что вы имели в виду 32 бит. Я сомневаюсь, что en.wikipedia.org/wiki/ZEBRA_%28computer%29 мог запустить Java, и я сомневаюсь, что с тех пор существует 33-битная машина.
chx
@chx совершенно очевидно, потому что я написал 32 бита раньше :)
Данубский моряк
11

Ниже приводится выдержка из сообщения об ошибке Oracle 6430675 по адресу. Посетите отчет для полного объяснения.

Методы {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.

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

shiv.mymail
источник