关于C#:比较浮点值有多危险?

How dangerous is it to compare floating point values?

我知道UIKit使用CGFloat,因为它是独立于分辨率的坐标系。

但每次我想检查,例如,frame.origin.x是否是0,都会让我感到恶心:

1
2
3
if (theView.frame.origin.x == 0) {
    // do important operation
}

==<=>=<>相比,CGFloat是否容易出现假阳性?这是一个浮点数,它们存在不可预测的问题:例如,0.0000000000041

当比较时,Objective-C是否在内部处理这一点,或者如果一个读为零的origin.x0相比,是否会发生这种情况?


首先,浮点值的行为不是"随机的"。精确的比较可以而且确实在许多现实世界中有意义。但是如果你要使用浮点,你需要知道它是如何工作的。假设浮点数的工作方式与实数类似,这会使代码很快中断。错误的一面是,假设浮点结果具有与之相关的大型随机模糊(就像这里的大多数答案所建议的那样),将会得到一个代码,该代码起初似乎可以工作,但最终会出现较大的数量级错误和断角情况。好的。

首先,如果要用浮点编程,应阅读以下内容:好的。

每一个计算机科学家都应该知道什么是浮点运算好的。

是的,全部读完。如果负担太大,您应该使用整数/固定点进行计算,直到有时间阅读为止。-)好的。

现在,尽管如此,精确的浮点比较的最大问题归结为:好的。

  • 事实上,您可以在源代码中写入或使用scanfstrtod读取的大量值不以浮点值的形式存在,并且会静默地转换为最接近的近似值。这就是魔鬼9733的答案。好的。

  • 由于没有足够的精度来表示实际结果,许多结果被舍入。您可以看到的一个简单示例是将x = 0x1fffffey = 1添加为float。在这里,x在尾数(ok)中有24位精度,y只有1位精度,但是当你添加它们时,它们的位不在重叠的地方,结果需要25位精度。相反,它被四舍五入(在默认的四舍五入模式下为0x2000000)。好的。

  • 由于需要无限多的位置才能得到正确的值,所以许多结果被舍入。这既包括1/3这样的有理结果(你很熟悉十进制中取无穷多个位置),也包括1/10(二进制中也取无穷多个位置,因为5不是2的幂),以及不合理的结果,比如任何不完美平方的平方根。好的。

  • 双圆角。在某些系统(尤其是x86)上,浮点表达式的计算精度高于其名义类型。这意味着,当上述四舍五入类型之一发生时,您将得到两个四舍五入步骤,首先将结果四舍五入为更高精度类型,然后四舍五入为最终类型。例如,考虑一下如果将1.49舍入到整数(1)中会发生什么,而如果先将其舍入到一个小数位(1.5),然后将结果舍入到整数(2)中会发生什么。这实际上是浮点处理的最糟糕的领域之一,因为编译器的行为(尤其是对于有缺陷的、不符合规范的编译器,如gcc)是不可预测的。好的。

  • 超越函数(trigexplog等)没有被指定为具有正确的四舍五入结果;结果只是被指定为在最后一个精确位置(通常称为1ulp)的一个单位内是正确的。好的。

  • 当您编写浮点代码时,需要记住您对可能导致结果不精确的数字所做的操作,并进行相应的比较。通常情况下,与"epsilon"比较是有意义的,但是epsilon应该基于所比较的数字的大小,而不是绝对常数。(在绝对常数epsilon可以工作的情况下,这强烈表明固定点(而不是浮点)是工作的正确工具!)好的。

    编辑:特别是,幅度相对epsilon检查应该如下所示:好的。

    1
    if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

    如果FLT_EPSILON是来自float.h的常数(用DBL_EPSILONfor doubles或LDBL_EPSILON替换为long doubles),并且K是一个常数,您选择这样计算的累积误差在最后一个地方肯定受K单位的限制(如果您不确定是否得到误差限制c计算正确,使K比你的计算所说的要大几倍)。好的。

    最后,请注意,如果使用它,可能需要在接近零的位置进行一些特别的注意,因为FLT_EPSILON对于非规范化没有意义。一个快速的解决办法是:好的。

    1
    if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

    同样,如果使用双打,则用DBL_MIN代替。好的。好啊。


    因为0完全可以表示为IEEE754浮点数(或者使用我曾经使用过的任何其他F-P数字实现),所以与0进行比较可能是安全的。但是,如果您的程序计算一个值(如theView.frame.origin.x),您有理由相信该值应该是0,但您的计算不能保证是0,那么您可能会被咬。

    为了澄清一点,计算如下:

    1
    areal = 0.0

    将(除非您的语言或系统被破坏)创建一个值,使(areal==0.0)返回true,但另一个计算(如

    1
    areal = 1.386 - 2.1*(0.66)

    可能不会。

    如果你能向自己保证你的计算产生的值是0(而不仅仅是它们产生的值应该是0),那么你就可以继续把f-p值和0进行比较。如果你不能保证达到要求的程度,最好还是坚持"容忍的平等"的常规方法。

    在最坏的情况下,对f-p值的粗心比较可能是极其危险的:想想航空电子设备、武器制导、电厂运行、车辆导航,以及几乎所有计算符合现实世界的应用。

    对愤怒的小鸟来说,没有那么危险。


    我想给出一个与其他人不同的答案。他们很适合回答你所说的问题,但可能不适合你需要知道什么或你真正的问题是什么。

    图形中的浮点很好!但几乎没有必要直接比较浮动。你为什么要这么做?图形使用浮动定义间隔。如果一个float在一个同样由float定义的区间内,那么比较总是定义得很好,只需要保持一致,而不是精确!只要一个像素(这也是一个间隔!)可以分配,这是所有图形的需要。

    所以,如果你想测试你的点是否在一个[0..width[范围]之外,这很好。只要确保一致地定义包含。例如,始终定义内部是(x>=0&;x

    但是,如果您滥用图形坐标作为某种标志,例如查看窗口是否停靠,则不应执行此操作。使用与图形表示层分离的布尔标志。


    与零比较是一种安全的操作,只要零不是一个计算值(如上面的答案所述)。原因是零是浮点中一个完全可表示的数字。

    说的是完全可表示的值,你得到24位的范围在两个概念的力量(单精度)。所以1,2,4是完全可代表的,如.5,.25和.125。只要你所有的重要部分都是24位的,你就是黄金。所以10.625可以精确地表示。

    这很好,但在压力下会很快崩溃。两个场景浮现在脑海中:1)涉及计算时。不要相信那个sqrt(3)*sqrt(3)==3。只是不会那样。它可能不会像其他一些答案所暗示的那样在一个epsilon内。2)当涉及非2次幂(NPOT)时。所以这听起来可能很奇怪,但是0.1是一个无穷大的二进制级数,因此任何涉及这样一个数字的计算从一开始都是不精确的。

    (原来的问题提到了与零的比较。不要忘记-0.0也是一个完全有效的浮点值。)


    [正确答案]掩盖了选择K的事实。选择K与选择VISIBLE_SHIFT一样是临时的,但选择K并不明显,因为与VISIBLE_SHIFT不同,它不依赖于任何显示属性。因此,选择你的毒液-选择K或选择VISIBLE_SHIFT。这个答案提倡选择VISIBLE_SHIFT,然后证明了选择K的困难。

    正是由于舍入错误,不应将"精确"值的比较用于逻辑运算。在视觉显示器上的特定位置的情况下,如果位置为0.0或0.000000000003,则不可能有任何影响-这种差异对眼睛来说是不可见的。所以你的逻辑应该是:

    1
    2
    #define VISIBLE_SHIFT    0.0001        // for example
    if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

    然而,最终,"肉眼看不见"将取决于您的显示属性。如果可以上界显示(应该可以),那么选择VISIBLE_SHIFT作为上界的一部分。

    现在,正确的答案取决于K,所以让我们来探讨选择K。上面的"正确答案"是:

    K is a constant you choose such that the accumulated error of your
    computations is definitely bounded by K units in the last place (and
    if you're not sure you got the error bound calculation right, make K a
    few times bigger than what your calculations say it should be)

    所以我们需要K。如果获得K比选择我的VISIBLE_SHIFT更困难、更不直观,那么你将决定什么对你有用。为了找到K,我们将编写一个测试程序,该程序查看一组K值,以便我们了解它的行为。如果"正确答案"可用,那么如何选择K应该是显而易见的。不?

    我们将使用"正确答案"的详细信息:

    1
    if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

    让我们试试k的所有值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    #include <math.h>
    #include <float.h>
    #include <stdio.h>

    void main (void)
    {
      double x = 1e-13;
      double y = 0.0;

      double K = 1e22;
      int i = 0;

      for (; i < 32; i++, K = K/10.0)
        {
          printf ("K:%40.16lf ->", K);

          if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
            printf ("YES
    "
    );
          else
            printf ("NO
    "
    );
        }
    }
    ebg@ebg$ gcc -o test test.c
    ebg@ebg$ ./test
    K:10000000000000000000000.0000000000000000 -> YES
    K: 1000000000000000000000.0000000000000000 -> YES
    K:  100000000000000000000.0000000000000000 -> YES
    K:   10000000000000000000.0000000000000000 -> YES
    K:    1000000000000000000.0000000000000000 -> YES
    K:     100000000000000000.0000000000000000 -> YES
    K:      10000000000000000.0000000000000000 -> YES
    K:       1000000000000000.0000000000000000 -> NO
    K:        100000000000000.0000000000000000 -> NO
    K:         10000000000000.0000000000000000 -> NO
    K:          1000000000000.0000000000000000 -> NO
    K:           100000000000.0000000000000000 -> NO
    K:            10000000000.0000000000000000 -> NO
    K:             1000000000.0000000000000000 -> NO
    K:              100000000.0000000000000000 -> NO
    K:               10000000.0000000000000000 -> NO
    K:                1000000.0000000000000000 -> NO
    K:                 100000.0000000000000000 -> NO
    K:                  10000.0000000000000000 -> NO
    K:                   1000.0000000000000000 -> NO
    K:                    100.0000000000000000 -> NO
    K:                     10.0000000000000000 -> NO
    K:                      1.0000000000000000 -> NO
    K:                      0.1000000000000000 -> NO
    K:                      0.0100000000000000 -> NO
    K:                      0.0010000000000000 -> NO
    K:                      0.0001000000000000 -> NO
    K:                      0.0000100000000000 -> NO
    K:                      0.0000010000000000 -> NO
    K:                      0.0000001000000000 -> NO
    K:                      0.0000000100000000 -> NO
    K:                      0.0000000010000000 -> NO

    如果我希望1e-13为"零",那么k应该是1e16或更大。

    所以,我想说你有两个选择:

  • 按照我的建议,用工程判断"epsilon"的值来做一个简单的epsilon计算。如果你在做图形,"零"意味着一个"可见的变化",而不是检查你的视觉资产(图像等)并判断epsilon是什么。
  • 在你阅读了非货物文化答案的参考文献(并在这个过程中获得了博士学位)然后用你的非直觉判断来选择K之前,不要尝试任何浮点计算。

  • 正确的问题:如何比较可可中的点?

    正确答案:cgPointEqualTopoint()。

    另一个问题:两个计算值是否相同?

    这里的答案是:他们没有。

    如何检查它们是否接近?如果要检查它们是否接近,则不要使用cgPointEqualTopoint()。但是,不要检查它们是否接近。做一些在现实世界中有意义的事情,比如检查一个点是否在一条线之外,或者一个点是否在一个球体内。


    上次我检查C标准时,没有要求双精度浮点运算(总共64位,尾数53位)的精度高于该精度。但是,一些硬件可能在寄存器中执行更高精度的操作,并且该要求被解释为不需要清除低阶位(超出加载到寄存器中的数字精度)。所以你可能会得到意想不到的比较结果,就像这样,这取决于登记簿中最后一个睡在那里的人留下了什么。

    也就是说,尽管我每次看到它时都会努力将其删除,但我工作的机构中有许多C代码是使用gcc编译的,并在Linux上运行,很长一段时间内我们都没有注意到这些意外的结果。我不知道这是否是因为GCC正在为我们清除低阶位,80位寄存器不用于现代计算机上的这些操作,标准已经更改,或者什么。我想知道是否有人能引用章节。


    您可以使用这样的代码来比较浮点数和零:

    1
    2
    3
    if ((int)(theView.frame.origin.x * 100) == 0) {
        // do important operation
    }

    这将与0.1精度进行比较,在这种情况下,这足以满足cgfloat的要求。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

    BOOL isEqual = NO;

    NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
    NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

    isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

    return isEqual;

    }


    我认为正确的做法是将每个数字声明为一个对象,然后在该对象中定义三个内容:1)相等运算符。2)设定可接受差分法。3)值本身。如果两个值的绝对差小于设置为可接受的值,则相等运算符返回true。

    您可以对对象进行子类化以适应问题。例如,如果圆钢直径相差小于0.0001英寸,则1至2英寸之间的金属圆钢可被视为直径相等。所以您可以使用参数0.0001调用setAcceptableDifference,然后使用带信心的相等运算符。