比较浮点值有多危险?
我知道UIKit
使用CGFloat
是因为独立于坐标系的分辨率。
但每次我想检查是否例如frame.origin.x
是0
它使我感到不舒服:
if (theView.frame.origin.x == 0) { // do important operation }
与==
, <=
, >=
, <
, >
比较, CGFloat
是否容易受到误报? 这是一个浮点,他们有无法解决的问题:例如, 0.0000000000041
。
Objective-C
是在比较内部还是在内部处理这个问题,或者可能会发生这样的情况: origin.x
读取为零,而不是与0
比较为真?
首先,浮点值在行为上不是“随机”的。 精确的比较在大量现实世界中可以并且确实有意义。 但是如果你要使用浮点,你需要知道它是如何工作的。 在假设浮点运算方面,如真实数字,会让你的代码很快崩溃。 在假定浮点结果的一边有大量的随机模糊(就像这里提供的大多数答案一样)会让你看起来起作用的代码,但最终会出现大幅度的错误和拐angular的情况。
首先,如果你想用浮点编程,你应该读这个:
计算机科学家应该知道什么是浮点运算
是的,全部阅读。 如果这太重了,你应该使用整数/定点进行计算,直到有时间读取它为止。 🙂
现在,就这样说,精确浮点比较的最大问题归结为:
-
事实上,你可能会在源代码中写入很多值,或者使用
scanf
或strtod
读入这些值,这些值不会以浮点值的forms存在,而是会悄然转换为最接近的近似值。 这就是demon9733的答案是在说什么。 -
由于没有足够的精度来表示实际结果,所以许多结果变得圆整。 一个简单的例子,你可以看到这是添加
x = 0x1fffffe
和y = 1
作为花车。 这里,x
在尾数(ok)中有24位的精度,而y
只有1位,但是当它们相加时,它们的位不在重叠的位置,结果需要25位精度。 取而代之的是舍入(在默认舍入模式下为0x2000000
)。 -
由于需要无限多的地方以获得正确的价值,所以许多结果变得圆整。 这包括三分之一的合理结果(你十分熟悉的十进制无限多的地方),还有1/10(二进制无限多的地方,因为5不是2的幂)以及不合理的结果,如不是一个完美的广场的东西的平方根。
-
双舍入。 在某些系统(尤其是x86)上,浮点expression式的评估精度要高于其标称types。 这意味着当上述types的舍入之一发生时,您将得到两个舍入步骤,首先将结果舍入到更高精度types,然后舍入到最终types。 作为一个例子,考虑如果将1.49舍入到整数(1),十进制会发生什么情况,而如果将第一个舍入到一位小数(1.5),然后将结果舍入到整数(2),会发生什么情况。 这实际上是浮点处理最麻烦的地方之一,因为编译器的行为(特别是对于错误的,不合格的编译器如GCC)是不可预知的。
-
超越函数(
trig
,exp
,log
等)没有被指定为具有正确舍入的结果; 结果只是在精确的最后一个单位(通常被称为1ulp )的一个单位内被指定为正确的。
当你编写浮点代码时,你需要记住你正在处理的数字可能会导致结果不精确,并进行相应的比较。 通常与“epsilon”进行比较是有意义的,但是这个epsilon应该基于你所比较的数字的大小 ,而不是一个绝对常数。 (如果绝对常数ε能够工作,那么强烈表明固定点,而不是浮点,是工作的正确工具!)
编辑:特别是,幅度相对的epsilon检查应该是这样的:
if (fabs(xy) < K * FLT_EPSILON * fabs(x+y))
其中FLT_EPSILON
是float.h
的常量(将其DBL_EPSILON
为double
s的LDBL_EPSILON
或long double
s的LDBL_EPSILON
),并且K
是一个常数,这样计算的累积误差肯定以最后K
单位为界如果你不确定是否有误差计算的权利,使K
比计算所说的要大几倍)。
最后,请注意,如果您使用此function,则可能需要特别小心,因为FLT_EPSILON
对于非FLT_EPSILON
则FLT_EPSILON
没有意义。 一个快速的解决办法是做到这一点:
if (fabs(xy) < K * FLT_EPSILON * fabs(x+y) || fabs(xy) < FLT_MIN)
如果使用双打也可以replaceDBL_MIN
。
由于0可以完全表示为一个IEEE754浮点数(或者使用任何其他的我曾经使用的fp数的实现),因此与0进行比较可能是安全的。 但是,如果你的程序计算出了一个值(比如theView.frame.origin.x
),你有理由相信它应该是0,但是你的计算不能保证是0。
澄清一点,计算如:
areal = 0.0
将会(除非你的语言或系统被破坏)创build一个值,使得(areal == 0.0)返回true,但另一个计算如
areal = 1.386 - 2.1*(0.66)
不得。
如果你可以保证你的计算产生的值是0(而不只是它们产生的值应该是0),那么你可以继续前进,并将fp值与0进行比较。如果你不能保证达到要求的程度最好坚持“宽容平等”的平常做法。
在最坏的情况下,fp值的粗心大意的比较可能是非常危险的:认为航空电子设备,武器指导,发电厂运营,车辆导航,几乎所有计算符合现实世界的应用。
对于愤怒的小鸟,不是那么危险。
我想给出一些不同于其他人的答案。 他们是伟大的回答你所说的问题,但可能不是你需要知道的或你真正的问题是什么。
浮点在graphics是好的! 但几乎没有必要直接比较花车。 你为什么需要这样做? graphics使用浮动来定义间隔。 并且比较一个浮点数是否在一个由浮点数定义的区间内总是被很好地定义,并且只需要是一致的,不准确的或者精确的! 只要一个像素(也是一个间隔!)可以分配这是所有的graphics需求。
所以如果你想testing你的观点是否超出[0..width [范围这就好了。 只要确保你一致地定义包含。 例如,总是定义里面是(x> = 0 && x <宽度)。 相交或命中testing也是如此。
但是,如果您正在滥用graphics坐标作为某种标志,例如查看窗口是否停靠,则不应该这样做。 改为使用与graphics表示层分离的布尔标志。
只要零不是一个计算值(如上面的答案所述),与零相比可以是安全的操作。 这是因为零是浮点数的完美表示。
说完全可表示的值,你可以得到24位的范围在一个幂的概念(单精度)。 因此,1,2,4是完全可以表示的,.5,.25和.125也是。 只要你所有的重要位都是24位的,你就是金。 所以10.625可以精确地表示出来。
这很好,但在压力下很快就会崩溃。 有两种情况值得思考:1)涉及到计算时。 不要相信sqrt(3)* sqrt(3)== 3.它不会那样。 正如其他一些答案所暗示的,它可能不会在一个小数内。 2)当涉及非功率2(NPOT)时。 所以这可能听起来很奇怪,但是0.1是二进制的无限序列,因此任何涉及这样的数字的计算从一开始就是不精确的。
(哦,原来的问题提到了比较零。不要忘记,-0.0也是一个完全有效的浮点值。)
[“正确的答案”掩盖了selectK
selectK
最终与selectVISIBLE_SHIFT
一样特别,但是selectK
并不明显,因为与VISIBLE_SHIFT
不同,它不依赖任何显示属性。 因此select你的毒药 – selectK
或selectVISIBLE_SHIFT
。 这个答案主张selectVISIBLE_SHIFT
,然后演示selectK
的难度,
正因为有圆的错误,所以不应该使用逻辑运算的“确切”值的比较。 在具体的视觉显示位置上,如果位置是0.0或0.0000000003,那么这个差别是不可见的。 所以你的逻辑应该是这样的:
#define VISIBLE_SHIFT 0.0001 // for example if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }
但是,最后,“看不见的眼睛”取决于你的显示属性。 如果你可以上限显示(你应该可以); 然后selectVISIBLE_SHIFT
作为该上限的一部分。
现在,“正确的答案”取决于K
所以我们来探讨selectK
。 上面的“正确答案”说:
K是你select的一个常数,所以你的计算的累积误差肯定是由K单位在最后的地方(如果你不确定你有错误界限计算的权利,使K的几倍大于你的计算说应该是)
所以我们需要K
如果获得K
更难,比select我的VISIBLE_SHIFT
更直观,那么你会决定什么适合你。 为了findK
我们将编写一个testing程序,查看一系列K
值,以便了解它的行为。 如果“正确的答案”是可用的,那么应该明白如何selectK
没有?
我们将用作“正确答案”的细节:
if (fabs(xy) < K * DBL_EPSILON * fabs(x+y) || fabs(xy) < 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(xy) < K * DBL_EPSILON * fabs(x+y) || fabs(xy) < 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或更大。
所以,我会说你有两个select:
- 正如我所build议的那样,使用您的工程判断为“epsilon”的值做一个简单的epsilon计算。 如果你正在做graphics,那么“零”就意味着是一个“明显的变化”,而不是检查你的视觉资产(图像等),并判断可以是什么。
- 在阅读非货物难题答案的参考资料(并在过程中获得博士学位)之前,不要尝试进行任何浮点计算,然后使用非直觉判断来select
K
正确的问题:如何比较Cocoa Touch中的点?
正确的答案:CGPointEqualToPoint()。
另外一个问题:两个计算值是一样的吗?
答案贴在这里:他们不是。
如何检查他们是否接近? 如果你想检查它们是否接近,那么不要使用CGPointEqualToPoint()。 但是,不要检查他们是否接近。 在现实世界中做一些有意义的事情,比如检查一个点是否超出一条线,或者一个点是否在一个球体内。
我最后一次检查C标准时,并不要求双精度浮点运算(总共64位,53位尾数)的精度要高于精度。 但是,某些硬件可能会在更高精度的寄存器中执行操作,并且要求被解释为不需要清除低位(超出加载到寄存器中的数字的精度)。 所以你可以得到意想不到的比较结果,这取决于最后睡在那里的寄存器中剩下的内容。
这就是说,尽pipe我每次看到它的时候都会努力删除它,但是我工作的这个装备有很多用gcc编译并运行在linux上的C代码,而且我们在很长一段时间内没有注意到这些意外的结果。 我不知道这是否是因为海湾合作委员会正在清除我们的低位,80位寄存器不用于现代计算机上的这些操作,标准已经改变,或者是什么。 我想知道是否有人可以引用章节和诗句。
你可以使用这样的代码来比较float和零:
if ((int)(theView.frame.origin.x * 100) == 0) { // do important operation }
这将与0.1的准确度进行比较,这对于CGFloat来说已经足够了。
我认为正确的做法是将每个数字声明为一个对象,然后在该对象中定义三件事情:1)一个相等运算符。 2)setAcceptableDifference方法。 3)价值本身。 如果两个值的绝对差值小于设置的可接受值,则相等运算符返回true。
您可以inheritance对象以适应问题。 例如,如果直径相差小于0.0001英寸,则1至2英寸之间的金属圆棒可被认为具有相同的直径。 所以你可以用参数0.0001调用setAcceptableDifference,然后使用相等运算符。