首页 文章

比较浮点值有多危险?

提问于
浏览
374

我知道 UIKit 使用 CGFloat 因为分辨率独立坐标系 .

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

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

==<=>=<> 比较时, CGFloat 是否容易受到误报?它是一个浮点,它们有不寻常的问题:例如 0.0000000000041 .

Objective-C 在比较时是否在内部处理此问题,或者发生读取为零的 origin.x0 无法比较为真?

10 回答

  • 13

    首先,浮点值的行为不是“随机”的 . 在大量现实世界的用法中,精确的比较可以而且确实有意义 . 但是如果你要使用浮点数,你需要知道它是如何工作的 . 假设浮点工作就像实数一样,会让你快速破解代码 . 假设浮点结果与它们相关的大量随机模糊(就像这里建议的大多数答案一样)会让你看起来最初工作的代码,但最终会出现大幅度错误和破坏角落情况 .

    首先,如果你想用浮点编程,你应该读这个:

    What Every Computer Scientist Should Know About Floating-Point Arithmetic

    是的,阅读全部内容 . 如果这是一个太大的负担,你应该使用整数/不动点进行计算,直到你有时间阅读它 . :-)

    现在,有了这个说法,精确浮点比较的最大问题归结为:

    • 您可以在源中编写或使用 scanfstrtod 读取的大量值不作为浮点值存在并以静默方式转换为最接近的近似值 . 这就是demon9733的答案所说的 .

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

    • 由于需要无限多的位置来获得正确的值,许多结果都会被舍入 . 这包括合理的结果,如1/3(你从十进制熟悉,它需要无限多的地方),但也是1/10(这也是二进制中无限多的地方,因为5不是2的幂),以及不合理的结果,如任何不完美正方形的平方根 .

    • 双舍入 . 在某些系统(特别是x86)上,浮点表达式的精度高于其标称类型 . 这意味着当上述类型的舍入之一发生时,您将获得两个舍入步骤,首先将结果舍入到更高精度类型,然后舍入到最终类型 . 例如,如果将1.49舍入为整数(1),请考虑以十进制发生的情况,而如果首先将其舍入到一个小数位(1.5),然后将结果舍入为整数(2),则会发生什么 . 这实际上是浮点处理的最糟糕的区域之一,因为编译器的行为(特别是对于有缺陷的,不符合要求的编译器,如GCC)是不可预测的 .

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

    当您使用可能导致结果不准确的数字时,并相应地进行比较 . 通常,与"epsilon"进行比较是有意义的,但是epsilon应该基于您所比较的数字的大小,而不是绝对常数 . (在绝对常数epsilon可以工作的情况下,这强烈表明固定点,而不是浮点,是工作的正确工具!)

    Edit: 特别是,幅度相对的epsilon检查应该类似于:

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

    FLT_EPSILON 是来自 float.h 的常量(用 double 代替 doubleLDBL_EPSILON 代替 long double )和 K 是一个常数,你选择这样你的计算累积误差是在最后一个地方肯定受到 K 单位的限制(如果你不确定你的错误绑定计算是正确的,那么使 K 比你的计算所说的要大几倍) .

    最后,请注意,如果你使用它,可能需要在零附近做一些特别小心,因为 FLT_EPSILON 对于非正规数没有意义 . 快速解决方法是:

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

    如果使用双打,同样替代 DBL_MIN .

  • 0

    由于0可以完全表示为IEEE754浮点数(或使用我曾经使用过的任何其他f-p数的实现),因此与0进行比较可能是安全的 . 但是,如果你的程序计算一个你有理由认为应该为0但你的计算不能保证为0的值(例如 theView.frame.origin.x ),你可能会被咬 .

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

    areal = 0.0
    

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

    areal = 1.386 - 2.1*(0.66)
    

    不得 .

    如果你可以向自己保证你的计算产生0的值(而不仅仅是它们产生的值应该是0)那么你可以继续将fp值与0进行比较 . 如果你不能保证自己达到所需的程度,最好坚持“宽容平等”的惯常做法 .

    在最糟糕的情况下,对f-p值的粗心比较可能是非常危险的:思考航空电子设备,武器引导,发电厂运行,车辆导航,几乎任何计算符合现实世界的应用 .

    对于愤怒的小鸟,没有那么危险 .

  • 10

    我想给出一些与其他答案不同的答案 . 它们非常适合回答您所述的问题,但可能不适合您需要了解的内容或您的真正问题 .

    图形中的浮点很好!但是几乎没有必要直接比较浮子 . 你为什么要这么做? Graphics使用浮点数来定义间隔 . 并且比较浮点是否也在浮点数定义的区间内总是很明确,只需要一致,不准确或精确!只要一个像素(也是一个间隔!)可以分配所有图形需要 .

    因此,如果你想测试你的点是否在[0..width [范围之外]这是正常的 . 只需确保一致地定义包含 . 例如,始终定义内部是(x> = 0 && x <width) . 交叉点或命中测试也是如此 .

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

  • -6

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

    说出完全可表示的值,您可以获得24位的功率范围(单精度) . 因此,1,2,4是完全可表示的,如.5,.25和.125 . 只要你的所有重要位都是24位,你就是金色的 . 所以10.625可以准确地表达 .

    这很好,但会在压力下迅速崩溃 . 我想到两种情况:1)涉及计算时 . 不要相信sqrt(3)* sqrt(3)== 3.它不会那样 . 并且它可能不会在epsilon中,正如其他一些答案所暗示的那样 . 2)当涉及任何非幂2(NPOT)时 . 所以它可能听起来很奇怪,但0.1是二进制的无限级数,因此涉及这样的数字的任何计算从一开始就是不精确的 .

    (哦,原始问题提到比较为零 . 不要忘记-0.0也是一个完全有效的浮点值 . )

  • 1

    ['right answer'掩饰选择 K . 选择 K 最终会像选择 VISIBLE_SHIFT 一样临时,但选择 K 不太明显,因为与 VISIBLE_SHIFT 不同,它不依赖于任何显示属性 . 因此,选择你的毒药 - 选择 K 或选择 VISIBLE_SHIFT . 这个答案提倡选择 VISIBLE_SHIFT 然后证明选择 K 的困难

    正是由于圆形错误,您不应该使用逻辑运算的“精确”值进行比较 . 在您在视觉显示器上的位置的特定情况下,如果位置是0.0或0.0000000003则无关紧要 - 眼睛看不到差异 . 所以你的逻辑应该是这样的:

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

    但是,最后,'invisible to the eye'将取决于您的显示属性 . 如果你可以上限显示(你应该能够);然后选择 VISIBLE_SHIFT 为该上限的一部分 .

    现在,'right answer'依赖于 K 所以让我们探索选择 K . 上面的'right answer'说:

    K是你选择的一个常数,这样你的计算的累积误差肯定是最后一个K单位的界限(如果你不确定你的误差界限计算是正确的,那么使K比你的大几倍大)计算说它应该是)

    所以我们需要 K . 如果获得 K 比选择我的_2632344更难,更不直观,那么你将决定什么对你有用 . 为了找到 K ,我们将编写一个测试程序来查看一堆 K 值,以便我们可以看到它的行为方式 . 如果'right answer'可用,应该明白如何选择 K . 没有?

    我们将使用,作为“正确答案”的详细信息:

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

    让我们试试K的所有值:

    #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\n");
          else
            printf ("NO\n");
        }
    }
    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或更大 .

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

    • 使用 engineering judgement 进行简单的epsilon计算,得到'epsilon'的值,因为我检查你的视觉资产(图像等)并判断epsilon是什么 .

    • 唐't attempt any floating point computations until you'已阅读非货物崇拜答案的参考(并在此过程中获得了博士学位),然后使用您的非直觉判断选择 K .

  • 448

    正确的问题:如何在Cocoa Touch中比较积分?

    正确答案:CGPointEqualToPoint() .

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

    答案贴在这里:他们不是 .

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

  • 20

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

    也就是说,尽管我每次看到它都会努力消除它,但是我工作的装备有很多C代码,它们是用gcc编译的并在linux上运行,我们在很长一段时间内都没有发现任何这些意想不到的结果 . 我不知道这是因为gcc正在为我们清除低位,80位寄存器不能用于现代计算机上的这些操作,标准已经改变,或者是什么 . 我想知道是否有人可以引用章节和诗句 .

  • 34

    您可以使用此类代码将float与零进行比较:

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

    这将与0.1准确度进行比较,在这种情况下对于CGFloat来说足够了 .

  • 4
    -(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;
    

    }

  • 5

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

    您可以将对象子类化以适应问题 . 例如,如果直径相差小于0.0001英寸,则认为1至2英寸的金属圆棒具有相同的直径 . 因此,您将使用参数0.0001调用setAcceptableDifference,然后充满信心地使用相等运算符 .

相关问题