意外的浮点运算

时间:2013-07-18 15:57:58

标签: c floating-point

前段时间我在这里发布了一个question,我知道由于精度不同,浮点值不应该与双值进行比较,我们可能总是得不到可预测的结果。但最近我偶然发现了另一个代码,其中两个或多个浮点数之间的比较也导致了非常奇怪的行为。

以下是我遇到的代码:

#include<stdio.h>
int main()
{
    float a=0.0f;
    int i;
    for(i=0;i<10;i++)
        a=a+0.1f;
    if(a==1.0f) printf("True\n");
    else printf("False\n");
    a=0.0f;
    for(i=0;i<5;i++)
        a=a+0.2f;
    if(a==1.0f) printf("True\n");
    else printf("False\n");
}

代码给了我False和true作为输出让我大吃一惊。为什么会这样? 如果精度损失,因为数字0.1f没有表示为二进制表示的精确度,并且一次又一次地添加它会导致总和小于1.0f?对于下一个循环,这应该是正确的吗?我们能在多大程度上信任浮点艺术?

2 个答案:

答案 0 :(得分:2)

您认为a+0.2等于a+0.1+0.1。对于某些a值,情况并非如此(因为舍入错误),但其他情况则属于这种情况。例如,a==0显然两者都相等,但如果aa+0.1==a的最小数字,则显然两者都不同。

请参阅此代码:

#include<stdio.h>
int main()
{
  float a=0.0f;
  int i;
  for(i=0;i<10;i++){
    if (a+0.1f+0.1f==a+0.2f)
      printf("i=%d, equal!\n",i);
    else
      printf("i=%d, delta=%.10f\n",i,(a+0.1f+0.1f)-(a+0.2f));
    a=a+0.1f;
  }
  return 0;
}

输出结果为:

i=0, equal!
i=1, equal!
i=2, equal!
i=3, equal!
i=4, equal!
i=5, delta=0.0000000596
i=6, delta=0.0000000596
i=7, delta=0.0000000596
i=8, equal!
i=9, equal!

答案 1 :(得分:1)

浮点数(包括C中的float和double)由两部分表示,两部分都有固定的位数来保存它们的值:

  1. 一个称为尾数的二进制小数(在二进制点的左边没有位),在二进制点的右边没有零。 (这与数字的十进制表示形式进行比较。小数点左侧的数字可以与二进制点左侧的位进行比较,小数点右侧的小数位数与小数点的二进制位数进行比较。二进制点的权利)。
  2. 一个指数,表示2乘以该尾数乘以2的幂。 (将其与科学记数法相比较0.1e5有5作为指数,表示10乘以10的幂乘以尾数)
  3. 在十进制中,我们不能用固定数量的小数位表示小数1/3。例如,0.333333并不完全等于1/3,因为3需要无限重复。

    在二进制中,我们不能用固定数量的小数位表示小数1/10。在这种情况下,二进制数0.00011001100110011不完全等于1/10,因为0011需要无限重复。因此,当1/10转换为浮点时,该部分将被切断以适合可用位。

    在二进制中,分母(底部)被10整除的任何分数都是无限重复的。这意味着很多浮点值都是不精确的。

    添加时,它们不准确。如果你将它们加在一起,那么不精确性可能取消或强化,这取决于当我们将无限重复的二进制分数转换为具有固定位数的二进制分数时,被切断的位中的值是什么。

    你也会得到大数字,含有大量数字的分数或添加非常不同的数字时的不精确性。例如,10亿加上.0000009无法用可用的位数表示,因此分数将被舍去。

    你可以看到它变得复杂。在任何特定情况下,您都可以提出浮点表示,评估由于截断位而导致的误差以及乘法或除法时的舍入。此时,如果你遇到麻烦,你可以确切地看出为什么会出错。

    简化示例 - 不精确的表示

    这是一个忽略指数并使尾数未标准化的示例,这意味着不会删除左侧的零。 (在7位后斩波时,0.0001100 = 1/10和0.0011001 = 1/20)请注意,在实际情况下,问题会在右侧出现更多数字:

    0.0001100 = 1/10
    0.0001100
    0.0001100
    0.0001100                             0.0011001 = 2/10 (1/5)
    0.0001100                             0.0011001
    0.0001100                             0.0011001
    ---------                             ---------
           00 <- sum of right 2 columns          11 <- sum of right column
        11000 <- sum of next column             00  <- sum of next two columns  
       110 <- sum of next column              11 <- sum of next column
      000 <- sum of other columns            11 <- sum of next column
    -------                                000 <- sum of other columns
    0.1001000 <- sum                      ---------
                                          0.1001011 <- sum
    

    我们可能会遇到与0.12345678901234567890这样的分数相同的问题,这些分数不适合我的例子中的7位。

    该怎么做

    首先,请记住浮点数可能不准确。增加或减少,甚至更多,乘法或除法应该会产生不精确的结果。

    其次,在比较两个浮点(或双精度)值时,最好将差值与某些“epsilon”进行比较。因此,如果天堂禁止,你将美元计算存储在浮点变量中,它就会像这样。我们不关心任何不到半美分的事情:

    if(fabsf(f1-f2)&gt; = 0.005f)...

    这意味着这些数字彼此接近,并且为了您的目的,足够接近。 (@EricPostpischil指出,没有“足够接近”的一般定义。它与你的计算希望完成的事情有关。)

    对一些小值进行比较会处理在进行某些浮点运算后可能位于低小数位的所有松散位。

    请注意,如果您与常量进行比较,它看起来很相似:

    if (fabsf(f1 - 1.0f) >= 0.000001f) ...
    

    或者您可以通过两次比较来检查相同的差异范围:

    if (f1 < 0.999999f || f1 > 1.000001f) ...
    

    我应该再次指出,每个问题都有自己的有趣小数位数。

    例如,如果谷歌告诉你地球上两个位置相隔多远的公里数,你可能会关注最近的水表,所以你说任何两个位置在0.001(千分之一公里)之内的功能相同。将差异与0.0005进行比较。或者您可能只关心最近的区块,因此将差异与0.03(300米)进行比较。因此,将差异与0.015进行比较。

    当您的测量工具非常准确时,同样适用。如果用尺度测量,不要指望结果精确到1/100英寸。