比较浮点值有多危险?

我知道UIKit使用CGFloat,因为分辨率无关的坐标系。

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

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

==<=>=<>相比,CGFloat不容易出现假阳性吗? 它是一个浮点数,它们有不精确的问题:例如0.0000000000041

在比较时,Objective-C是否在内部处理这个问题,或者是否会发生读取为0的origin.x0相比不为真?

97118 次浏览

[“正确答案”掩盖了选择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 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

啊,所以K应该是1e16或者更大如果我想让1e13等于0。

所以,我认为你有两个选择:

  1. 像我建议的那样,使用< em > < / em >工程判断对'epsilon'的值进行简单的计算。如果你做的是图形,“零”意味着“可见的变化”,那就检查你的视觉资产(图像等)并判断epsilon可以是什么。
  2. 不要尝试任何浮点计算,直到你阅读了非cargo-cult答案的参考(并在此过程中获得博士学位),然后使用你的非直觉判断来选择K

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

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

areal = 0.0

会(除非你的语言或系统坏了)创建一个值(areal==0.0)返回true,但另一个计算,如

areal = 1.386 - 2.1*(0.66)

可能不会。

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

在最坏的情况下,不小心比较f-p值可能是极其危险的:想想航空电子设备、武器制导、发电厂操作、车辆导航,几乎所有计算与现实世界相结合的应用。

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

首先,浮点值的行为不是“随机的”。精确的比较在现实世界的许多用法中是有意义的。但是如果你要使用浮点数你需要知道它是如何工作的。错误地假设浮点数像实数一样工作,将使您的代码很快崩溃。如果假设浮点结果有大量的随机模糊(就像这里的大多数答案所建议的那样),会让你的代码一开始看起来可以工作,但最终会出现巨大的错误和崩溃的角落情况。

首先,如果你想用浮点数编程,你应该读一下:

每个计算机科学家都应该知道的浮点运算 .

是的,通读一遍。如果这对你来说负担太大,你应该使用整数/固定点来计算,直到你有时间阅读它。: -)

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

  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这样有bug的、不符合标准的编译器)是不可预测的。

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

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

编辑:具体地说,相对大小的epsilon检查应该看起来像这样:

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

其中FLT_EPSILON是来自float.h的常数(将其替换为DBL_EPSILON用于__abc3s或long doubles),而K是一个你选择的常数,这样你的计算的累积误差肯定是由K单位限定的(如果你不确定你得到的误差范围计算是正确的,让K比你的计算说的应该大几倍)。

最后,请注意,如果你使用这个,在接近零的地方可能需要一些特别的注意,因为FLT_EPSILON对于非法线没有意义。一个快速的解决方法是:

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

如果使用双精度变量,同样替换DBL_MIN

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

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

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

也就是说,尽管我一看到它就努力删除它,但我工作的机构有大量使用gcc编译并运行在linux上的C代码,我们已经很长时间没有注意到这些意想不到的结果了。我不知道这是否是因为gcc为我们清除了低阶位,80位寄存器在现代计算机上不用于这些操作,标准已经改变了,还是什么。我想知道是否有人可以引用章节和诗句。

与0相比可以是一个安全的操作,只要0不是一个计算值(如上面的回答所述)。这样做的原因是0在浮点数中是一个完全可表示的数字。

谈到完全可表示的值,您可以在2的幂概念中获得24位范围(单精度)。所以12 4是完全可表示的,。5。25和。125也是。只要你所有重要的比特都是24比特的,你就是黄金。所以10.625可以被精确地表示出来。

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

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

我想给出一个和其他人不一样的答案。他们很好地回答了你的问题,但可能不是你需要知道的或你真正的问题是什么。

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

所以如果你想测试你的点是否在a [0..]宽度[范围,这很好。只要确保你对包含的定义是一致的。例如,内部总是定义为(x>=0 &&x & lt;宽度)。这同样适用于交叉测试或命中测试。

但是,如果您滥用图形坐标作为某种标志,例如查看窗口是否停靠,则不应该这样做。使用一个独立于图形表示层的布尔标志。

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

正确答案:CGPointEqualToPoint()。

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

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

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

你可以使用这样的代码来比较float和0:

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

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

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

我使用下面的比较函数来比较小数点后的数位:

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
}

另一个可能需要记住的问题是,不同的实现做事情的方式不同。我非常熟悉的一个例子是索尼Playstation 2上的FP单元。与任何X86设备中的IEEE FP硬件相比,它们具有显著的差异。引用的文章提到完全缺乏对inf和NaN的支持,而且情况变得更糟。

不太为人所知的是我后来所知道的“一位乘数”。错误。对于float x的某些值:

    y = x * 1.0;
assert(y == x);

断言将失败。在一般情况下,有时,但不总是,在Playstation 2上FP相乘的结果比等效的IEEE尾数少一位。

我的观点是,您不应该假设将FP代码从一个平台移植到另一个平台会产生相同的结果。任何给定的平台内部一致,在该平台上的结果不会改变,只是它们可能与另一个平台不一致。例如,X86上的CPython使用64位双精度来表示浮点数,而Cortex MO上的CircuitPython必须使用软件FP,并且只使用32位浮点数。不用说,这会引起差异。

我40多年前学到的一句话今天依然适用。在计算机上做浮点运算就像移动一堆沙子。你每做一件事,都会留下一点沙子,捡起一点泥土。

Playstation是索尼公司的注册商标。