ChatGPT解决这个技术问题 Extra ChatGPT

为什么 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 更新 31。

在 java 1.7.0 上可以正常工作i.imgur.com/hZeqx.png
@Adel:请参阅我对 Oli's answer 的评论,看起来 Java 6 实现了这一点(和 documents that it does),通过将 0.5 添加到数字然后使用 floor 可能会导致进一步的精度损失; Java 7 no longer documents it that way(大概/希望是因为他们修复了它)。
这是我编写的测试程序中的一个错误。 ;)
另一个显示浮点值的示例不能按面值计算。
想了想之后。我看不出有什么问题。 0.49999999999999994 大于小于 0.5 的最小可表示数字,并且十进制人类可读形式的表示本身就是试图欺骗我们的近似值。

O
Oliver Charlesworth

概括

在 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


@ Oli:哦,现在这很有趣,他们为 Java 7(我链接到的文档)去掉了这一点——也许是为了通过触发(进一步)精度损失来避免导致这种奇怪的行为。
我不禁认为这个修复只是装饰性的,因为零是最明显的。毫无疑问,还有许多其他浮点值会受到这种舍入误差的影响。
@MichaëlRoy 实际上,没有。这是一个特定的数字。没有其他数字小于 n + 1/2 但足够接近 n + 1/2,因此添加 x + 1/2 得到 n + 1 而不是更小的数字。
@gnasher 我不同意。出于这个原因,舍入取决于域。出于这个原因,对于科学应用程序和金融应用程序,您不会以相同的方式对数字进行四舍五入。例如:对图形或 DSP 使用除 floor(x + .5f) 以外的任何东西将不可避免地导致灾难。这可能也是为什么 round() 从未在科学应用程序中使用的原因。
S
Simon Nickerson

这似乎是已在 Java 7 中修复的已知错误 (Java bug 6430675: Math.round has surprising behavior for 0x1.fffffffffffffp-2)。


+1:很好的发现!如我的回答中所述,与 Java 6 和 7 之间的文档差异相关。
P
Peter Mortensen

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 的最大 double 值或不。在这种情况下,数字不是小于 0.5 的最大双精度值,因此 else 块返回 0。

你可以试试 0.49999999999999999d,它会返回 1,但不会返回 0,因为这是小于 0.5 的最大双精度值。


那么这里的 1.499999999999999994 会发生什么?返回 2 ?它应该返回 1,但这应该会得到与之前相同的错误,但返回 1。 ?
1.499999999999999994 不能用双精度浮点数表示。 1.4999999999999998 是小于 1.5 的最小双倍。正如您从问题中看到的那样,floor 方法对其进行了正确舍入。
P
Peter Mortensen

我在 JDK 1.6 32 位上也有同样的情况,但在 Java 7 64 位上我有 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

在 Java 7(您用来测试它的版本)中,该错误已修复。
我想你的意思是32位。我怀疑 en.wikipedia.org/wiki/ZEBRA_%28computer%29 可以运行 Java,而且我怀疑从那以后是否有 33 位机器。
@chx 很明显,因为我之前写过 32 位 :)
P
Peter Mortensen

下面的答案是 Oracle bug report 6430675 at 的摘录。访问报告以获取完整说明。

{Math, StrictMath.round 方法在操作上定义为

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

对于双重论点。虽然此定义通常按预期工作,但对于 0x1.fffffffffffffp-2 (0.49999999999999994),它给出了令人惊讶的结果 1,而不是 0。

值 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 的中间值。在 Java 使用的 IEEE 754 算术舍入到最接近偶数舍入模式中,当浮点结果不精确时,两者越接近必须返回包含确切结果的可表示浮点值;如果两个值同样接近,则返回最后一位为零的值。在这种情况下,加法的正确返回值是 1,而不是小于 1 的最大值。

虽然该方法按定义运行,但此输入的行为非常令人惊讶;规范可以修改为更像“四舍五入到最接近的长,四舍五入”的内容,这将允许更改此输入的行为。