Чому 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.


1
На java 1.7.0 працює нормально i.imgur.com/hZeqx.png
Кава

2
@Adel: Дивіться мій коментар до відповіді Олі , схоже, що Java 6 реалізує це (і документи, які він робить ) таким чином, що може призвести до подальшої втрати точності, додавши 0.5до числа і потім скориставшись floor; Java 7 вже не документує це таким чином (мабуть / сподіваємось, що вони це виправили).
TJ Crowder

1
Це була помилка в тестовій програмі, яку я написав. ;)
Пітер Лоурі

1
Інший приклад, який показує значення з плаваючою комою, не може бути прийнятий за номіналом.
Michaël Roy

1
Подумавши про це. Я не бачу проблеми. 0.49999999999999994 є більшим за найменше представницьке число менше 0,5, а представлення у десятковій формі, прочитаній людиною, є самим наближенням, яке намагається нас обдурити.
Michaël Roy

Відповіді:


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
@ Oli: О, це цікаво, вони взяли цей біт для Java 7 (документи, з якими я пов’язаний) - можливо, щоб уникнути подібного дивного поведінки, викликавши (подальше) втрату точності.
TJ Crowder

@TJCrowder: Так, це цікаво. Чи знаєте ви, чи є якісь документи "примітки до випуску" / "удосконалення" для окремих версій Java, щоб ми могли перевірити це припущення?
Олівер Чарлсуорт


1
Я не можу не думати, що це виправлення є лише косметичним, оскільки нуль найбільше видно. Немає сумніву, що багато інших значень з плаваючою комою впливають на цю помилку округлення.
Michaël Roy

232

Здається, це відома помилка (помилка Java 6430675: Math.round має дивовижну поведінку для 0x1.ffffffffffffffff-2 ), яка була виправлена ​​в Java 7.


5
+1: Приємна знахідка! Пов'язується з відмінностями в документації між Java 6 та 7, як пояснено у моїй відповіді.
Олівер Чарльворт


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 воно буде викликати підлогу і, отже, повертає 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

У мене те саме в JDK 1.6 32-розрядний, але на Java 7 64-розрядний я отримав 0 для 0,49999999999999994, який округлий дорівнює 0, а останній рядок не друкується. Здається, це проблема VM, однак, використовуючи плаваючі точки, слід очікувати, що результати дещо відрізнятимуться в різних середовищах (процесор, 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.fffffffffffffffp-2 (0,49999999999999994).

Значення 0,49999999999999994 - найбільше значення з плаваючою комою менше 0,5. Як шістнадцятковий буквал з плаваючою комою його значення дорівнює 0x1.fffffffffffffffffp-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.

Хоча метод працює як визначено, поведінка на цьому вході дуже дивно; специфікація може бути змінена на щось подібне до "Раунд до найближчих довгих, закруглювальних зв'язків", що дозволило б змінити поведінку на цьому вході.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.