比较 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) 并且断言也通过了。
谁能更详细地解释一下:
- 为什么断言通过 eps = 1.0E-13 而不是通过 eps = 1.0E-14
- 为什么断言通过 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 - eps
是 9.99999999
。到目前为止,一切都很好。然而,10.0 - 9.99999999
是 0.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.
为什么这个断言在 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) 并且断言也通过了。
谁能更详细地解释一下:
- 为什么断言通过 eps = 1.0E-13 而不是通过 eps = 1.0E-14
- 为什么断言通过 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 - eps
是 9.99999999
。到目前为止,一切都很好。然而,10.0 - 9.99999999
是 0.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.