Math.pow() in Java 8 和 Java 13 returns 不同的结果

Math.pow() in Java 8 and Java 13 returns different result

以下代码 returns 在 Java 8 和 Java 11 上的不同结果。

class Playground {
    public static void main(String[ ] args) {
        long x = 29218;
        long q = 4761432;
        double ret = Math.pow(1.0 + (double) x / q, 0.0005);
        System.out.println("val = " + String.format("%.24f", ret));
    }
}

Java 8:

val = 1.000003058823805400000000

Java 11:(与Python,Rust 的结果相同)

val = 1.000003058823805100000000

问题是这些:

如果我们只使用 Double.toString() 来打印答案,那么两个不同的结果将是

1.0000030588238054
1.0000030588238051

末尾的额外数字只是 String.format() 的一个因素。我们看到数字仅在最后一个有效的小数位上有所不同。将这两个数字转换为十六进制给出

1.000033518c576
1.000033518c575

所以二进制表示仅在最后一位 (ulp) 相差一个单位。

阅读 Math.pow 的规范,我们发现

“计算结果必须在精确结果的 1 ulp 以内。”

真实值接近 1.000003058823805246468 (WolframAlpha),介于两个答案之间,因此都在规格范围内。

所有发生的事情是图书馆在算法上有轻微的变化,也许是为了让它更快。

注意

System.out.println("val = " + String.format("%.24f", ret));

printf 样式格式、字符串连接和 println 的不必要组合。可以先用printf

System.out.printf("val = %.24f%n", ret);

但是,当 double 精度甚至无法远程提供那么多数字时,请求 24 位十进制数字是没有意义的。当你使用

System.out.println("val = " + ret);

相反,它将默认为实际可用的数字,这会产生

val = 1.0000030588238054

对于 Java 8 和

val = 1.0000030588238051

对于 Java 11。

所以区别只在最后一位。或者更准确地说

double d1 = 1.0000030588238051, d2 = 1.0000030588238054;
System.out.println((d2 - d1) == Math.ulp(d1));

打印 true,因此这两个值之间的距离是 double 可能的最小距离。它们之间没有其他 double 值。 specification of pow says:

The computed result must be within 1 ulp of the exact result.

由于上面的代码表明,两个结果的距离均为 1 ulp,因此当确切结果位于两个结果之间时,两个结果都是正确的。 Wolfram Alpha 表示,确切的结果以

开头
1.000003058823805246…

所以它介于这两个结果之间。因此,根据规范,这两个结果都是正确的。

为了便于比较:

1.0000030588238054         JDK  8
1.000003058823805246…      Wolfram Alpha
1.0000030588238051         JDK 11

经过一些调查,快速的答案是 strictfp。参考:https://en.wikipedia.org/wiki/Strictfp

在 java8 中,默认行为是在 x86 机器上使用 x87 FPU,这将使用扩展双精度(80 位)作为中间值。所以结果将不能保证在另一个平台上是相同的值。

在 java11 或 java13 中,默认行为是使用 IEEE 双精度(64 位)作为中间值。

由于几乎所有现代语言都通过 IEEE-floats 保证稳定的结果,这种行为很难在其他语言或新的 java 中模仿。内联 x87 FPU 汇编可能会有所帮助。