【问题标题】:c++ float subtraction rounding errorc ++浮点减法舍入错误
【发布时间】:2014-08-21 19:17:28
【问题描述】:

我有一个介于 0 和 1 之间的浮点值。我需要将它用 -120 转换为 80。 为此,首先我在减去 120 后乘以 200。 当进行减法时,我有舍入错误。 让我们看看我的例子。

    float val = 0.6050f;
    val *= 200.f;

正如我所料,现在 val 是 121.0。

    val -= 120.0f;    

现在 val 是 0.99999992

我想也许我可以通过乘法和除法来避免这个问题。

    float val = 0.6050f;
    val *= 200.f;
    val *= 100.f;
    val -= 12000.0f;    
    val /= 100.f;

但这没有帮助。我手上还有 0.99。

有解决办法吗?

编辑:在详细记录之后,我知道这部分代码没有问题。在我的日志显示“0.605”之前,在我有详细的日志并且我看到“0.60499995946884155273437500000000000000000000000000”之后 问题出在不同的地方。

Edit2:我想我找到了罪魁祸首。初始值为 0.5750。

std::string floatToStr(double d)
{
    std::stringstream ss;
    ss << std::fixed << std::setprecision(15) << d;
    return ss.str();
}

int main()
{    
    float val88 = 0.57500000000f;
    std::cout << floatToStr(val88) << std::endl;
}

结果是 0.574999988079071

实际上,我每次都需要从这个值中加上和减去 0.0025。 通常我期望 0.575, 0.5775, 0.5800, 0.5825 ....

Edit3:实际上我用双倍尝试了所有这些。它适用于我的示例。

std::string doubleToStr(double d)
{
    std::stringstream ss;
    ss << std::fixed << std::setprecision(15) << d;
    return ss.str();
}

int main()
{    
    double val88 = 0.575;
    std::cout << doubleToStr(val88) << std::endl;
    val88 += 0.0025;
    std::cout << doubleToStr(val88) << std::endl;
    val88 += 0.0025;
    std::cout << doubleToStr(val88) << std::endl;
    val88 += 0.0025;
    std::cout << doubleToStr(val88) << std::endl;

    return 0;
}

结果是:

0.575000000000000
0.577500000000000
0.580000000000000
0.582500000000000

但不幸的是,我一定会漂浮。我需要改变很多东西。

感谢大家的帮助。

Edit4:我找到了我的字符串解决方案。我使用 ostringstream 的舍入并在此之后转换为双精度。我可以有 4 个精确的正确数字。

std::string doubleToStr(double d, int precision)
{
    std::stringstream ss;
    ss << std::fixed << std::setprecision(precision) << d;
    return ss.str();
}

    double val945 = (double)0.575f;
    std::cout << doubleToStr(val945, 4) << std::endl;
    std::cout << doubleToStr(val945, 15) << std::endl;
    std::cout << atof(doubleToStr(val945, 4).c_str()) << std::endl;

结果是:

0.5750
0.574999988079071
0.575

【问题讨论】:

  • 太糟糕了,它是浮点数。总会有不准确的地方。
  • 0.6050.f 是语法错误。我想你的意思是0.6050f。请复制并粘贴确切的代码。
  • 我无法重现您的结果。那不是你真正的代码,是吗?
  • 另外两美分,解决这个问题的唯一方法是不使用浮点数。根据定义,浮点数是近似值,但您正在查看的数字范围都可以存储在 32 位定点数中。

标签: c++ floating-point


【解决方案1】:

让我们假设您的编译器完全针对 floatdouble 值和操作实现 IEEE 754 binary32 和 binary64。

首先,您必须了解0.6050f 不代表数学量6050 / 10000。它恰好是0.605000019073486328125,最接近的float。即使您从那里编写完美的计算,您也必须记住这些计算是从 0.605000019073486328125 而不是从 0.6050 开始的。

其次,您可以通过使用double 计算并最终转换为float 来解决几乎所有累积的舍入问题:

$ cat t.c
#include <stdio.h>

int main(){
  printf("0.6050f is %.53f\n", 0.6050f);
  printf("%.53f\n", (float)((double)0.605f * 200. - 120.));
}

$ gcc t.c && ./a.out 
0.6050f is 0.60500001907348632812500000000000000000000000000000000
1.00000381469726562500000000000000000000000000000000000

在上面的代码中,所有的计算和中间值都是双精度的。

如果您记得您从 0.605000019073486328125 而不是 0.6050(不作为 float 存在)开始,那么 1.0000038… 是一个非常好的答案。

【讨论】:

  • 投反对票的人,有什么评论可以和投反对票一起去吗?上述任何事实错误?
  • 我用硬代码尝试了你的例子,0.6050f 来自另一个地方,但我输入了硬代码并且效果很好。我写了音频插件,但我不能使用 std::cout。我使用日志系统,当 0.6050f 到来时,我将日志系统放入一个文件中。它写“0.605”。但我认为我需要调试这个值。
  • @VolkanOzyilmaz 太好了。我的专长是 -1 分答案,这实际上帮助了提问者。
  • 难道不能从浮点数正确转换为双精度数吗?双 val945 = (双)0.575f; std::cout
  • @VolkanOzyilmaz 从floatdouble 的转换通常是无损的(“通常”的意思是“在与问题相同的假设下”)。 0.575f 实际上已经是接近 0.574999988079071 的值。您无法通过将信息转换为 double恢复信息,因为信息已经丢失。
【解决方案2】:

如果您真的关心 0.99999992 和 1.0 之间的差异,float 对于您的应用程序来说不够精确。您至少需要更改为double

如果您需要特定范围内的答案,并且您得到的答案略微超出该范围但在某个端点的舍入误差内,请将答案替换为适当的范围端点。

【讨论】:

    【解决方案3】:

    可以总结一下每个人的观点:一般来说,浮点是精确,但不是精确

    精确如何由尾数中的位数控制——浮点数为 24,双精度数为 53(假设 IEEE 754 二进制格式,这在当今非常安全![1 ])。

    如果您正在寻找一个精确结果,您必须准备好处理与该精确结果不同(非常微小)的值,但是...


    (1) 精确二进制分数问题

    ...第一个问题是您要查找的确切值是否可以准确以二进制浮点形式表示...

    ...这是罕见的——这通常是一个令人失望的惊喜。

    给定值的二进制浮点表示可以是精确的,但仅在以下受限情况下:

    • 该值是一个整数,

      这是最简单的情况,也许很明显。由于您正在查看 >= -120 和

    或:

    • 该值是一个整数,它被 2^n 整除,然后(如上)

      这包括第一条规则,但更通用。

    或:

    • 该值有小数部分,但当该值乘以生成整数所需的最小 2^n 时,该整数为

      这部分可能会让你大吃一惊。

      考虑 27.01,它是一个足够简单的十进制值,并且显然在浮点数的 ~7 十进制数字精度范围内。 不幸的是,它没有精确的二进制浮点形式——你可以将27.01乘以任何你喜欢的2^n,例如:

        27.01 * (2^ 6) =      1728.64   (multiply by  64)
        27.01 * (2^ 7) =      3457.28   (multiply by 128)
        ...
        27.01 * (2^10) =     27658.24
        ...
        27.01 * (2^20) =  28322037.76
        ...
        27.01 * (2^25) = 906305208.32  (> 2^24 !)
      

      而你永远不会得到一个整数,更不用说一个

      实际上,所有这些规则都归结为一个规则...如果您能找到一个“n”(正或负,整数),使得y = value * (2^n),其中y 是一个精确 em>, odd 整数,如果 y value 具有精确表示——假设没有下溢或溢出,这是另一个故事。

    这看起来很复杂,但经验法则很简单:“很少有十进制小数可以完全表示为二进制小数”。

    为了说明有多少,让我们考虑所有 4 位小数,其中有 10000,即 0.0000 到 0.9999——包括平凡的整数情况 0.0000。我们可以枚举其中有多少具有精确的二进制等价物:

       1: 0.0000 =  0/16 or 0/1
       2: 0.0625 =  1/16
       3: 0.1250 =  2/16 or 1/8
       4: 0.1875 =  3/16
       5: 0.2500 =  4/16 or 1/4
       6: 0.3125 =  5/16
       7: 0.3750 =  6/16 or 3/8
       8: 0.4375 =  7/16
       9: 0.5000 =  8/16 or 1/2
      10: 0.5625 =  9/16
      11: 0.6250 = 10/16 or 5/8
      12: 0.6875 = 11/16
      13: 0.7500 = 12/16 or 3/4
      14: 0.8125 = 13/16
      15: 0.8750 = 14/16 or 7/8
      16: 0.9375 = 15/16
    

    就是这样! 16/10000 可能的 4 位小数(包括 琐碎 0 的情况)在any强>精确。所有其他 9984/10000 可能的十进制小数都会产生 循环 二进制小数。因此,对于“n”位小数,只有 (2^n) / (10^n) 可以精确表示——那就是 1/(5^n) !!

    当然,这是因为您的小数部分实际上是有理数 x / (10^n)[2] 而您的二进制小数是 y / (2^m)(对于整数 x、y、n 和 m),并且对于给定的二进制小数完全等于我们必须有的小数:

      y = (x / (10^n)) * (2^m)   
        = (x / ( 5^n)) * (2^(m-n))
    

    只有当x(5^n) 的精确倍数时才会出现这种情况——否则y 不是整数。 (注意n m,假设x 没有(虚假的)尾随零,因此n 尽可能小。)


    (2) 舍入问题

    浮点运算的结果可能需要四舍五入到目标变量的精度。 IEEE 754 要求执行该操作,就好像对精度没有限制一样,然后将(“真”)结果四舍五入到目标精度处的最接近的值。因此,最终结果尽可能精确...考虑到参数的精确度以及目的地的精确度的限制...但不精确

    (对于浮点数和双精度数,'C' 可能在执行操作之前将浮点数参数提升为双精度数(或长双精度数),其结果将四舍五入为双精度数。然后表达式的最终结果可能是双精度数(或长双精度),如果要存储在浮点变量中,则对其进行舍入(再次)。所有这些都增加了乐趣!请参阅 FLT_EVAL_METHOD 了解您的系统所做的事情——注意浮点常量的默认值是双倍。)

    所以,要记住的其他规则是:

    • 浮点值不是实数(实际上,它们是具有有限分母的有理数)。

      浮点值的精度可能很大,但有很多实数无法精确表示!

    • 浮点表达式不是代数

      例如,将度数转换为弧度需要除以 ππ 的任何算术都有问题(因为它是不合理的),而对于浮点,π 的值会被四舍五入到我们使用的任何浮点精度。因此,将(比如说)27(精确)度转换为弧度需要除以 180(精确)和乘以我们的“π”。无论参数多么精确,除法和乘法都可能四舍五入,因此结果可能只是近似值。服用:

          float pi = 3.14159265358979 ;   /* plenty for float */
          float  x = 27.0 ;
          float  y = (x / 180.0) * pi ;
          float  z = (y / pi) * 180.0 ;
      
          printf("z-x = %+6.3e\n", z-x) ;
      

      我的(相当普通的)机器给出:“z-x = +1.907e-06”...所以,对于我们的浮点数:

      x != (((x / 180.0) * pi) / pi) * 180 ;
      

      至少,不是所有x。在所示情况下,相对差异很小 - ~ 1.2 / (2^24) - 但不是零,简单的代数可能会让我们期待。

    • 因此:浮点等式是一个狡猾的概念

      由于上述所有原因,两个浮点值的测试x == y 是有问题的。根据 xy 的计算方式,如果您期望两者完全相同,您可能会非常失望。


    [1] 十进制浮点数是有标准的,但一般人用的是二进制浮点数。

    [2] 对于任何小数,您都可以用有限的数字写下来!

    【讨论】:

      【解决方案4】:

      即使使用双精度,您也会遇到以下问题:

      200. * .60499999999999992 = 120.99999999999997
      

      您似乎需要某种类型的舍入,以便将 0.99999992 舍入为 1.00000000。

      如果目标是产生最接近 1/1000 倍数的值,请尝试:

      #include <math.h>
      
          val = (float) floor((200000.0f*val)-119999.5f)/1000.0f;
      

      如果目标是产生最接近 1/200 倍数的值,请尝试:

          val = (float) floor((40000.0f*val)-23999.5f)/200.0f;
      

      如果目标是产生最接近整数的值,请尝试:

          val = (float) floor((200.0f*val)-119.5f);
      

      【讨论】:

        猜你喜欢
        • 2012-09-11
        • 1970-01-01
        • 1970-01-01
        • 2011-06-09
        • 2014-07-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多