小数部分不太小的浮点不精确结果 - 有没有办法克服?

Floating point inexact result with not-too-small decimal fraction - is there a way to overcome?

我有一段代码重复递减一个从 1 开始的变量。经过两次迭代,结果不准确,这让我很惊讶。

有效过程可以用下面两行表示:

>> (1 - 0.1) - 0.9
0
>> (1 - 0.05 - 0.05) - 0.9
-1.110223024625157e-16

在 Matlab 和 Octave 中,第二种情况的结果都不为零。

同样,下面的C代码显示了问题:

#include <stdio.h>

void main () {
  double x1, x2;
  x1=1-0.05-0.05;
  x2=1-0.1;

  printf("x1 exact:%d, x2 exact:%d\n", x1==0.9, x2==0.9);
}

在 Intel Xeon(R) CPU E5-2680 上使用 gcc 4.7.0 版编译,结果为

x1 exact:0, x2 exact:1

显示第一个计算不准确,而第二个计算准确。

我可以在 C 中通过使用 long double(所有文字的后缀 "L",以及声明为 long double 的变量)来克服这个问题。这让我有些困惑:我认为不精确的结果可以用 0.05 在基数 2 中没有精确表示这一事实来解释;但是使用 long double 并没有改变这个事实...那么为什么结果不同呢?

我真正想要的是一种在 Matlab 中克服它的方法。有什么想法吗?

有趣的是,MS Excel 对同一计算的结果在两种情况下都是准确的。

如您所知,像 0.10.9 这样的十进制数字在二进制中没有精确的表示。所以如果你这样做:

float f = 0.1;
if(f * 9 == 0.9)
    printf("exact\n");
else
    printf("inexact\n");

,或者如果您像原始问题中那样编写代码,它可能会打印 "exact",也可能会打印 "inexact",这取决于...各种各样的事情。在我看来,不值得花太多时间试图找出原因或原因。如果它打印 "inexact",那么,我们知道为什么,它一开始就不准确。如果它打印 "exact",我们得到 "lucky" —— 某处的一些不精确性相互抵消了,或者什么的 —— 但我们不能依赖它,所以它不是很有趣。

既然我们不能指望它,我们必须编写代码在打印时执行适当的舍入,或者使用 "close enough" 相等比较。一旦我们编写了该代码,它就可以正常工作,而不管确切的比较是否偶然似乎有效。

… the result is … show that the first calculation is inexact while the second is exact.

这个推断是不正确的。 1 - .05 - .05 == .9 为假且 1 - .1 == .9 为真这一事实表明计算 1 - .05 - .05 时出现的舍入误差不会产生等于 .9 的结果,但舍入误差在计算 1 - .1 中出现的结果确实会产生等于 .9 的结果。由于在源代码中使用 .9 的浮点结果本身不是 .9,因此 1 - .1 等于 .9 并不意味着 1 - .1 是准确的。这只是意味着它得到了与 .9.

相同的舍入误差

表达式1 - .1 == .9中存在三个舍入错误:

  • .1从源代码中的十进制数字转换为二进制浮点数时,结果不是.1而是0.1000000000000000055511151231257827021181583404541015625.
  • .9由源码中的十进制数字转为二进制浮点数时,结果为0.90000000000000002220446049250313080847263336181640625.
  • 当前面的数字0.1000000000000000055511151231257827021181583404541015625减去1时,我们可以看到,因为它大于.1,所以结果应该在.9以下,比如.8999……。但是结果是0.90000000000000002220446049250313080847263336181640625.

恰好舍入误差在两边产生了相同的结果,所以相等性测试评估为真。

在计算1 - .05 - .05时,舍入误差为:

  • .05在源码中转换为0.05000000000000000277555756156289135105907917022705078125.
  • 1 - .05 的结果为 0.9499999999999999555910790149937383830547332763671875。
  • 1 - .05 - .05 的结果为 0.899999999999999911182158029987476766109466552734375`。

这里发生的事情是,在减去 1 - .05 时,碰巧有一个附近的可表示值略小于 .95。因此,当我们看到略大于 .05 的 .05 减去时,我们是从略小于 .95 的值中减去略大于 .05 的值。这种误差组合在 .9 下产生足够的数学结果,它更接近下一个较低的可表示值 0.899999999999999911182158029987476766109466552734375,而不是 0.90000000000000002220446049250313130346726

当你用long double做类似的计算时,舍入误差恰好不同。当以这些方式检查浮点计算时,long double 不会 double 计算结果恰好落在相同结果上的频率更好会得到理想的数学。在您的示例中,您选择的数字可能会发生这种情况,但使用其他数字会产生有效的随机结果,就好像每个计算都有一个向上或向下一个步骤的随机错误。如果这些错误恰好在两个不同的计算中具有相同的净效果,那么这两个结果是相等的。如果这些错误恰好具有不同的净效果,则两个结果不相等。