比较 Java 中的小双精度值

Comparing small double values in Java

为什么这个断言在 Java 中失败:

    double eps = 0.00000000000001;
    double ten = 10.0;
    double result = (ten - (ten - eps));
    Assert.assertTrue(result <= eps);

如果我在 eps 中的数字 1 之前删除一个零,则断言通过。我假设这与浮点实现有关,但我不确定具体是如何实现的。

此外,如果我将数字 1 替换为 2(例如 0.00000000000002),断言也会通过。在那种情况下,我什至可以在数字 2 之前添加更多的零,测试仍然会通过。我尝试使用 Double.MIN_VALUE (4.9E-324) 并且断言也通过了。

谁能更详细地解释一下:

  1. 为什么断言通过 eps = 1.0E-13 而不是通过 eps = 1.0E-14
  2. 为什么断言通过 eps = Double.MIN_VALUE (4.9E-324) 而不是 eps = 1.0E-14

编辑:当我将 eps 增加到 1.0E-8 时,断言也失败了:double eps = 0.00000001;

试试下面的代码:

BigDecimal eps1 = new BigDecimal(eps);
BigDecimal ten1 = new BigDecimal(ten);
BigDecimal result1 = ten1.subtract( ten1.subtract(eps1) );

无论eps如何都应该是稳定的

这是因为表示 double 类型的字节的组织。

如下图所示,它是一个64位结构。位 [b0 .. b51] 是 'concatenated' 并由指数提升,[b52 .. b62].

确定每个位组合在实际值中代表什么的等式是:

根据这个公式,最小值表示为

3ff0 0000 0000 000116   =>  1.0000000000000002

为了更好的解释,请参阅此 wiki 页面 Double-precision floating-point format

在最后一个断言中,您正在比较结果 (1.0658141036401503E-14) 和 eps (1.0E-14),从数学上看,从断言来看应该是错误的,在这种情况下,结果大于 eps。如果从 eps 中删除一个 0,则 rps 变为 1.0E-13,在这种情况下大于 1.0658141036401503E-14

问题是断言代码是 wrong-ish 在某种意义上它没有考虑第二次减法 ten - (ten - eps).

让我们一步步解释这个。让eps = 0.00000001 (1.0E-8)。在这种情况下,10.0 - eps9.99999999。到目前为止,一切都很好。然而,10.0 - 9.999999990.00000001000000082740371,它在 0.00000001 的预期结果附近,只是稍微大了一点,因为浮点运算(通常)给出了足够好的近似值。因此,对于某些 eps 值,最终结果非常接近,但略低于实际结果,对于某些值,它再次非常接近,但略高于实际结果

需要修正代码,以考虑到第二次减法的结果也只是一个近似值。

一种方法是将断言更改为:

Assert.assertTrue(Math.abs(result - eps) <= eps);

为了更好地理解浮点运算,我发现这篇文章写得很好:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

这句话总结了浮点运算错误发生的原因:

There are two reasons why a real number might not be exactly representable as a floating-point number. The most common situation is illustrated by the decimal number 0.1. Although it has a finite decimal representation, in binary it has an infinite repeating representation. Thus when β = 2, the number 0.1 lies strictly between two floating-point numbers and is exactly representable by neither of them.