比较浮点值有多危险?


391

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

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

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

是不是CGFloat容易误报比较时==<=>=<>?这是一个浮点,它们存在不精确的问题:0.0000000000041例如。

Objective-C比较时是否在内部处理此问题,还是会发生origin.x读为零的a与未读0为true的情况?

Answers:


466

首先,浮点值的行为不是“随机的”。精确的比较可以并且确实在大量实际使用中有意义。但是,如果您要使用浮点,则需要知道它是如何工作的。假设浮点运算像实数一样容易出错,这会使您的代码快速中断。假设浮点结果与它们相关联时有很大的随机模糊(如此处的大多数答案所示),这样做会出错,使您的代码起初看起来可以工作,但最终会出现大幅度错误和断角情况。

首先,如果要使用浮点编程,则应阅读以下内容:

每个计算机科学家都应了解的浮点运算法则

是的,请阅读所有内容。如果这太麻烦了,则应在计算之前使用整数/不动点进行计算。:-)

如此说来,精确浮点比较的最大问题归结为:

  1. 该地段价值的,你可以在源写,或与读取的事实scanf或者strtod不存在浮动点值并获得静悄悄地转换为最接近的近似。这就是demon9733的答案。

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

  3. 由于需要无限多个位置来获取正确的值,因此许多结果会四舍五入。这既包括合理的结果,例如1/3(您从十进制开始熟悉的位置,在该位置无数个地方),也包括1/10(由于二进制数的乘方,因为5不是2的幂,所以也无穷多个),以及任何非理想平方的平方根之类的非理性结果。

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

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

在编写浮点代码时,需要牢记您对可能导致结果不精确的数字所做的操作,并进行相应的比较。通常,与“ epsilon”进行比较会很有意义,但是该epsilon应该基于所比较的数字大小,而不是绝对常数。(在绝对恒定的ε起作用的情况下,这强烈表明定点而不是浮点是完成任务的正确工具!)

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

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

FLT_EPSILON从定float.h(含更换DBL_EPSILONdoubleS或LDBL_EPSILONlong doubleS)和K是您选择这样的计算的累积误差绝对是以下所界定的恒定K在最后的地方单位(如果你不知道你得到了错误绑定计算权,K比您的计算结果要大几倍)。

最后,请注意,如果使用此选项,则可能需要特别注意零附近的值,因为FLT_EPSILON这对于异常值没有意义。一个快速的解决方法是:

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

DBL_MIN如果使用双打,同样也要替换。


25
fabs(x+y)如果xy(可以)具有不同的符号是有问题的。仍然,这是对“货色”比较大潮的一个很好的答案。
丹尼尔·菲舍尔

27
如果xy具有不同的符号,那没有问题。右侧将“太小”,但是由于xy具有不同的符号,因此它们无论如何都不应该相等。(除非它们是如此之小以至于不正常,但随后第二种情况抓住了它)
R .. GitHub STOP HELPING ICE 2012年

4
我对您的说法感到很好奇:“特别是对于越野车,不符合要求的编译器(例如GCC)”。真的是海湾合作委员会越野车,还是不符合规定?
尼古拉斯Ozimica

3
由于该问题被标记为iOS,因此值得注意的是,苹果的编译器(包括clang和Apple的gcc构建)始终使用FLT_EVAL_METHOD = 0,并试图严格限制不携带过多的精度。如果发现任何违规行为,请提交错误报告。
斯蒂芬·佳能

17
“首先,浮点值的行为并非“随机”。在大量实际使用中,精确比较可以而且确实是有意义的。” -仅需两个句子,就已经获得+1!这是人们在使用浮点数时最令人不安的假设之一。
Christian Rau

36

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

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

areal = 0.0

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

areal = 1.386 - 2.1*(0.66)

不得。

如果您可以确保自己的计算得出的值为0(而不仅是它们得出的值应该为0),则可以继续将fp值与0进行比较。如果不能保证自己达到所需的程度, ,最好坚持“容忍平等”的通常做法。

在最坏的情况下,粗心地比较fp值可能会非常危险:考虑航空电子设备,武器制导,电厂操作,车辆导航,以及几乎任何能够满足实际情况的应用。

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


11
实际上,这1.30 - 2*(0.65)是一个表达式的完美示例,如果您的编译器实现IEEE 754,则该表达式的求值显然为0.0,因为以0.65和表示的双精度数1.30具有相同的有效位数,并且乘以2显然很精确。
Pascal Cuoq

7
仍然可以从中获得代表,因此我更改了第二个示例片段。
Performance Performance Mark

22

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

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

因此,如果要测试点是否在[0..width [范围外],就可以了。只要确保您一致地定义包含即可。例如,始终将内部定义为(x> = 0 && x <width)。相交或命中测试也是如此。

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


13

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

如果说出完美可表示的值,则可以得到2的幂(单精度)概念中的24位范围。因此,1、2、4是完全可表示的,.5,.25和.125也是。只要您所有的重要位都在24位中,您就是黄金。因此,可以精确表示10.625。

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

(哦,最初的问题提到了与零的比较。别忘了-0.0也是一个完全有效的浮点值。)


11

[“正确答案”掩盖了选择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) { /* ... */ }

但是,最后,“肉眼看不见”将取决于您的显示属性。如果可以使显示上限(应该可以);然后选择VISIBLE_SHIFT是该上限的一小部分。

现在,“正确答案”立足于此,K因此让我们探讨选择问题K。上面的“正确答案”说:

K是一个常数,您可以选择使计算的累积误差最后由K单位来限制(如果不确定不确定误差边界的计算是否正确,请使K比计算值大几倍)说应该)

所以我们需要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。
  2. 在您阅读完非载运性答案的参考文献(并在此过程中获得博士学位)然后使用非直觉判断来选择之前,请勿尝试任何浮点计算K

10
与分辨率无关的一个方面是,您不能确定在编译时有什么“可见的转变”。在超高清屏幕上不可见的内容在小屏幕上非常明显。至少应使其成为屏幕尺寸的函数。或命名其他。
罗曼(Romain)2012年

1
但是,至少选择“可见偏移”是基于易于理解的显示(或帧)属性-与<正确答案>不同K,后者很难选择且不直观。
GoZoner

5

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

正确答案:CGPointEqualToPoint()。

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

答案在这里发布:它们不是。

如何检查它们是否紧密?如果要检查它们是否关闭,则不要使用CGPointEqualToPoint()。但是,不要检查它们是否关闭。做一些在现实世界中有意义的事情,例如检查某个点是否在直线之外或某个点是否在球体内。


4

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

就是说,尽管我努力在每次看到它时都将其删除,但是我工作的公司还是有许多使用gcc编译并在linux上运行的C代码,而且很长一段时间我们都没有注意到这些意外结果。我不知道这是否是因为gcc正在为我们清除低序位,80位寄存器未用于现代计算机上的这些操作,标准已更改或其他原因。我想知道是否有人可以引用章节和经文。


1

您可以使用以下代码将浮点数与零进行比较:

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

这将与0.1精度进行比较,在这种情况下,对于CGFloat而言已经足够。


int没有保证的情况下强制转换theView.frame.origin.xint导致不确定行为(UB)的范围-在这种情况下,即的范围的1/100 int
chux-恢复莫妮卡

完全没有理由像这样转换为整数。正如chux所说,超出范围的值可能会带来UB;在某些架构上,这将比仅在浮点中进行计算要慢得多。最后,将其乘以100将得到0.01精度,而不是0.1。
Sneftel

0
-(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;

}


0

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

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
}

-6

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

您可以子类化对象以适合该问题。例如,如果直径在1-2英寸之间的金属圆棒的直径差异小于0.0001英寸,则可以认为它们相等。因此,您可以使用参数0.0001调用setAcceptableDifference,然后放心使用相等运算符。


1
这不是一个好答案。首先,整个“目标事物”对解决您的问题没有任何帮助。其次,实际上您对“平等”的实际实施不是正确的。
汤姆·斯威利

3
汤姆,也许您会再考虑“物体”。对于实数,以高精度表示,平等很少发生。但是,如果适合您,一个人的平等观念可能会量身定制。如果有一个可重写的“近似相等”运算符会更好,但没有。
约翰·怀特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.