5

我在 Perl 中有一个简单的 for 循环

for ($i=0; $i <= 360; $i += 0.01)
{
print "$i ";
}

为什么当我运行此代码时,我得到以下输出,一旦它达到 0.81,它就会突然开始添加更多小数位?我知道我可以简单地总结一下来避免这个问题,但我想知道为什么会这样。0.01 的增量似乎一点也不疯狂。

 0.77
 0.78
 0.79
 0.8
 0.81
 0.820000000000001
 0.830000000000001
 0.840000000000001
 0.850000000000001
 0.860000000000001
 0.870000000000001
4

4 回答 4

16

计算机使用二进制表示。并非所有十进制浮点数都以二进制表示法精确表示,因此可能会出现一些错误(实际上是舍入差异)。这与您不应将浮点数用于货币值的原因相同:

乱七八糟的收据

(图片取自dailywtf

解决此问题的最优雅方法是使用整数进行计算,将它们除以正确的小数位数并使用sprintf限制打印的小数位数。这将确保:

  • 总有正确的打印结果
  • 舍入误差不会累积

试试这个代码:

#!/usr/bin/perl
for ($i=0; $i <= 360*100; $i += 1) {
  printf "%.2f \n", $i/100;
}
于 2013-02-06T23:55:36.650 回答
7

基本上,因为十进制数 0.01 在二进制浮点中没有精确的表示,所以随着时间的推移,将最佳近似值添加到 0.01 会偏离您想要的答案。

这是(二进制)浮点运算的基本属性,并不是 Perl 特有的。每个计算机科学家都应该知道的关于浮点运算的知识是标准参考,您可以通过 Google 搜索轻松找到它。

另请参阅:C 编译器错误(浮点运算)以及毫无疑问的无数其他问题。


Kernighan 和 Plauger 在他们古老但经典的著作《编程风格的要素》中说:

  • 一位聪明的老程序员曾经说过“浮点数就像一堆小沙子,每移动一个,你就会失去一点沙子,得到一点泥土”。

他们还说:

  • 10 * 0.1 几乎不是 1.0

两种说法都指出浮点运算并不精确。

请注意,一些现代 CPU (IBM PowerPC) 内置了 IEEE 754:2008 十进制浮点运算。如果 Perl 使用了正确的类型(可能没有),那么您的计算将是准确的。

于 2013-02-06T23:50:40.603 回答
4

为了演示 Jonathan 的答案,如果您在 C 中编写相同的循环结构,您将得到相同的结果。尽管 C 和 Perl 的编译方式可能不同并且在不同的机器上运行,但底层的浮点算术规则应该会产生一致的输出。注意:Perl 使用双精度浮点作为其浮点表示,而在 C 中,编码器显式选择 float 或 double。

在 C 中循环:

    #include <stdio.h>

    int main() {
        double i;
        for(i=0;i<=1;i+=.01)  {
          printf("%.15f\n",i);
        } 
      }

输出:

    0.790000000000000
    0.800000000000000
    0.810000000000000
    0.820000000000001
    0.830000000000001
    0.840000000000001
    0.850000000000001

为了进一步证明这一点,用 C 编写循环代码,但现在使用单精度浮点运算,并看到输出不太精确,甚至更不稳定。

输出:

    0.000000000000000
    0.009999999776483 
    0.019999999552965
    0.029999999329448
    0.039999999105930
    0.050000000745058
    0.060000002384186
    0.070000000298023
    0.079999998211861
    0.089999996125698
    0.099999994039536
于 2013-02-07T00:02:26.203 回答
2

1/10 在二进制中是周期性的,就像 1/3 在十进制中是周期性的。因此,它不能准确地存储在浮点数中。

>perl -E"say sprintf '%.17f', 0.1"
0.10000000000000001

要么使用整数

for (0*100..360*100) {
   my $i = $_/100;
   print "$i ";
}

或者做很多四舍五入

for (my $i=0; $i <= 360; $i = sprintf('%.2f', $i + 0.01)) {
   print "$i ";
}
于 2013-02-07T00:02:17.723 回答