比较浮点值有多危险?

时间:2012-04-26 13:41:13

标签: objective-c ios c floating-point floating-accuracy

我知道UIKit因使用独立于分辨率的坐标系而使用CGFloat

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

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

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

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

10 个答案:

答案 0 :(得分:450)

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

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

What Every Computer Scientist Should Know About Floating-Point Arithmetic

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

现在,据说,精确浮点比较的最大问题归结为:

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

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

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

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

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

  6. 当您编写浮点代码时,您需要记住您正在对可能导致结果不准确的数字执行的操作,并相应地进行比较。通常,与“epsilon”进行比较是有意义的,但是epsilon应该基于您正在比较的数字的幅度,而不是绝对常数。 (如果绝对常数epsilon可以工作,那就强烈表明固定点,而不是浮点,是工作的正确工具!)

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

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

    其中FLT_EPSILON是来自float.h的常量(替换为DBL_EPSILON的{​​{1}}或double的{​​{1}}和{ {1}}是你选择的一个常数,这样你的计算的累积误差肯定会被最后一个LDBL_EPSILON个单位所限制(如果你不确定你得到了错误绑定计算权,那么make {{ 1}}比你的计算所说的要大几倍。

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

    K
    如果使用双打,

    同样替换K

答案 1 :(得分:34)

由于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值的粗心比较可能是非常危险的:想想航空电子设备,武器引导,发电厂运行,车辆导航,几乎任何计算符合现实世界的应用。

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

答案 2 :(得分:20)

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

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

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

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

答案 3 :(得分:13)

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

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

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

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

答案 4 :(得分:10)

['正确答案'掩盖了选择K。选择K最终会像选择VISIBLE_SHIFT一样临时,但选择K不太明显,因为与VISIBLE_SHIFT不同,它不基于任何显示属性。因此,选择你的毒药 - 选择K或选择VISIBLE_SHIFT。这个答案提倡选择VISIBLE_SHIFT,然后展示选择K]

的难度

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

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

然而,最终,“眼睛看不见”将取决于您的显示属性。如果你可以上限显示(你应该能够);然后选择VISIBLE_SHIFT作为该上限的一部分。

现在,'正确答案'取决于K,所以让我们来探索挑选K。上面的“正确答案”说:

  

K是你选择的常数,这样你的累积误差   计算肯定是最后一个K单位的界限(和   如果您不确定错误绑定计算是否正确,请将K设为a   比你的计算所说的要大几倍)

所以我们需要K。如果获取K比选择VISIBLE_SHIFT更困难,更不直观,那么您将决定什么对您有用。要查找K,我们将编写一个测试程序来查看一堆K值,以便我们可以看到它的行为方式。如果'正确答案'可用,应该明白如何选择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或更大。

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

  1. 使用 工程判断 对'epsilon'的值进行简单的epsilon计算,正如我所建议的那样。如果你正在做图形和“零”意味着是一个“可见的变化”,而不是检查你的视觉资产(图像等),并判断epsilon可以是什么。
  2. 在您阅读非货物评论的答案(并在此过程中获得博士学位)之后,不要尝试任何浮点计算,然后使用您的非直观判断来选择K

答案 5 :(得分:5)

正确的问题:如何比较Cocoa Touch中的点数?

正确答案:CGPointEqualToPoint()。

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

答案在这里张贴:他们不是。

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

答案 6 :(得分:4)

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

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

答案 7 :(得分:1)

您可以使用此类代码将float浮动为零:

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

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

答案 8 :(得分:-1)

我正在使用以下比较功能来比较小数位数:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

答案 9 :(得分:-6)

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

您可以将对象子类化以适应问题。例如,如果它们的直径相差小于0.0001英寸,则认为1到2英寸之间的金属圆棒具有相同的直径。所以你用参数0.0001调用setAcceptableDifference,然后放心地使用相等运算符。