是浮点数==永远不错?

就在今天,我遇到了一个我们正在使用的第三方软件,在他们的示例代码中有这样的话:

// defined in somewhere.h static const double BAR = 3.14; // code elsewhere.cpp void foo(double d) { if (d == BAR) ... } 

我意识到浮点和它们的表示法的问题,但它让我想知道是否有情况下float == float会好吗? 我不是在问什么时候工作,而是在什么时候工作。

另外,像foo(BAR)这样的电话怎么样? 这将总是比较相等,因为他们都使用相同的static const BAR

有两种方法可以回答这个问题:

  1. 是否有float == float给出正确结果的情况?
  2. 有没有情况下, float == float是可以接受的编码?

(1)的答案是:有,有时。 但是它会变得脆弱,导致对(2)的回答:不,不要那样做。 你在未来乞求奇怪的错误。

至于foo(BAR)forms的调用:在特定情况下,比较将返回true,但是当你写foo你不知道(也不应该依赖)它是如何调用的。 例如,调用foo(BAR)将会很好,但是foo(BAR * 2.0 / 2.0) (甚至可能是foo(BAR * 1.0)取决于编译器优化的东西)将会中断。 你不应该依靠调用者不执行任何算术!

长话短说,即使a == b将在某些情况下工作,你真的不应该依靠它。 即使你今天可以保证调用语义,也许你下周不能保证他们,所以省下一些痛苦,不要使用==

在我看来, float == float永远不会* OK,因为它几乎不可维护。

*对于从不小的价值。

是的,你保证整数,包括0.0,与==相比

当然,你必须要小心一点,你如何得到整个号码,分配是安全的,但任何计算的结果是可疑的

ps有一组真实的数字确实有一个完美的复制作为一个浮动(想到1/2,1/4 1/8等),但你可能不知道你有这些之一。

只是为了澄清。 IEEE 754保证在整个范围内浮点整数(整数)是精确的。

 float a=1.0; float b=1.0; a==b // true 

但是你必须小心如何得到整个数字

 float a=1.0/3.0; a*3.0 == 1.0 // not true !! 

其他答案很好地解释了为什么使用==为fp号码是危险的。 我相信,我刚刚发现了一个很好地说明这些危险的例子。

在x86平台上,对于某些计算,您可能会得到奇怪的fp结果,这不是由于您执行的计算固有的舍入问题。 这个简单的C程序有时会打印“错误”:

 #include <stdio.h> void test(double x, double y) { const double y2 = x + 1.0; if (y != y2) printf("error\n"); } void main() { const double x = .012; const double y = x + 1.0; test(x, y); } 

该程序基本上只是计算

 x = 0.012 + 1.0; y = 0.012 + 1.0; 

(只传播两个函数和中间variables),但比较仍然可以产生错误!

原因是在x86平台上,程序通常使用x87 FPU进行FP计算。 x87在内部计算的精度比普通的double ,所以double值在存入内存时需要四舍五入。 这意味着x87 – > RAM – > x87的往返运行失去精度,因此计算结果会因中间结果是否经过RAM或是否全部停留在FPU寄存器而有所不同。 这当然是一个编译器的决定,所以这个bug只会在某些编译器和优化设置中出现:-(。

有关详细信息,请参阅GCC错误: http ://gcc.gnu.org/bugzilla/show_bug.cgi?id= 323

挺吓人的

附加说明:

这种types的错误通常会非常棘手的debugging,因为不同的值一旦碰到RAM就会变得相同。

所以如果你比如扩展了上面的程序来实际打印yy2的比特模式,你将会得到完全相同的值 。 要打印该值,必须将其加载到RAM中以传递给printf等某些打印function,这将使差异消失…

我将尽力为float等于或多或less提供合法,有意义和有用的testing的真实例子。

 #include <stdio.h> #include <math.h> /* let's try to numerically solve a simple equation F(x)=0 */ double F(double x) { return 2*cos(x) - pow(1.2, x); } /* I'll use a well-known, simple&slow but extremely smart method to do this */ double bisection(double range_start, double range_end) { double a = range_start; double d = range_end - range_start; int counter = 0; while(a != a+d) // <-- WHOA!! { d /= 2.0; if(F(a)*F(a+d) > 0) /* test for same sign */ a = a+d; ++counter; } printf("%d iterations done\n", counter); return a; } int main() { /* we must be sure that the root can be found in [0.0, 2.0] */ printf("F(0.0)=%.17f, F(2.0)=%.17f\n", F(0.0), F(2.0)); double x = bisection(0.0, 2.0); printf("the root is near %.17f, F(%.17f)=%.17f\n", x, x, F(x)); } 

我不想解释使用的二分法 ,而是强调停止条件。 它具有正确的讨论forms: (a == a+d)其中两边都是浮点数: a是我们当前的方程根的近似值, d是我们当前的精度。 给定algorithm的前提条件 – 在range_startrange_end之间必须存在一个根 – 我们保证每次迭代时根都保持在aa+d之间,而d在每一步都减半,缩小边界。

然后,经过多次迭代后, d变得非常小 ,以至于在加法时它变为零! 也就是说, a+d变得更接近于 其他的浮动 ; 所以FPU将其转化为最接近的值:对a本身。 这可以通过在假想的计算机上计算来容易地说明。 让它有4位小数尾数和一些大的指数范围。 那么结果是什么结果应该给2.131e+02 + 7.000e-3 ? 确切的答案是213.107 ,但我们的机器不能代表这样的数字; 它必须围绕它。 而213.107213.1更接近213.1 – 所以取整的结果变成2.131e+02 – 小加数消失,四舍五入到零。 在我们的algorithm的某些迭代中, 保证完全相同 – 在那一点上,我们不能再继续下去了。 我们已经find最大可能的精度的根源。

这个启发性的结论显然是浮动的。 他们看起来非常像真实的数字,每个程序员都想把它们看成真实的数字。 但他们不是。 他们有自己的行为,有点让人想起真实的,但不完全相同。 你需要非常小心,特别是在比较平等的时候。


更新

经过一段时间的回顾,我也注意到一个有趣的事实:在上面的algorithm中,在停止条件下实际上不能使用“一些小数字” 。 对于任何数字的select,都会有input,这会导致您的select太大 ,导致精度的损失, 并且会有input将认为您的select太小 ,导致过多的迭代甚至进入无限循环。 详细讨论如下。

你可能已经知道微积分没有“小数目”的概念:对于任何实数,你都可以很容易地find无限多的小数。 问题是那些“更小”的可能是我们实际寻求的东西; 这可能是我们等式的根源。 更糟糕的是,对于不同的方程,可能有不同的根源(例如2.51e-81.38e-8 ),如果我们的停止条件看起来像d < 1e-6 ,那么这两个 1.38e-8 将用相同的数字近似。 无论select哪一个“小数”,根据“ a == a+d停止条件的最大精度find的许多根,都会因“epsilon” 太大而变坏。

但是,在浮点数中指数的范围是有限的,所以实际上可以find最小的非零正FP编号(例如IEEE 754单精度FP的1e-45 denorm)。 但是没用! while (d < 1e-45) {...}将永远循环,假设单精度(正非零) d

撇开那些病态的边缘情况,在d < eps停止条件下的“小数”的select对许多方程来说太小了。 在那些指数足够高的方程中,只有最低有效位数的两个尾数相减的结果很容易超过我们的“epsilon”。 例如,使用6位尾数7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000 ,这意味着指数+8和5位尾数之间的最小可能差值是.. 1000! 这将永远不适合,例如, 1e-4 。 对于这些指数相对较高的数字,我们根本没有足够的精度来看到1e-4的差异。

我上面的实现也考虑了这个最后一个问题,你可以看到d每步减半,而不是重新计算(可能在指数中有很大的差异) ab 。 所以,如果我们将停止条件改为d < eps ,那么algorithm不会卡在具有巨大根的无限循环中(它可以很好地与(ba) < eps )相(ba) < eps ,但是仍然会在收缩期间执行不必要的迭代精确度。

这种推理可能看起来过于理论化,不必要的深入,但其目的是再次说明浮游物的诡计。 在编写算术运算符时,应该非常小心它们的有限精度。

完美的整数值,即使在浮点格式

但简短的回答是: “不,不要使用==”。

具有讽刺意味的是,当在格式范围内对整数值进行操作时,浮点格式“完美地”工作,即具有精确的精度。 这意味着,如果你坚持双重价值观,你会得到完美的整数,有点超过50位,给你大约四十五亿五千万,或四十五万亿。

实际上,这就是JavaScript在内部的工作原理,这就是为什么JavaScript可以在+-上实现大数字,而在32位上只能使用<<>>

严格来说,您可以准确地比较数字的总和和产品。 那些将是所有的整数,加上由1/2个n项组成的分数。 所以,循环递增n + 0.25,n + 0.50n + 0.75就没有问题了,但是其他96个小数点后面的两个数字都没有。

所以答案是: 在狭义的情况下,理论上的确切平等在理论上是有意义的,但最好避免。

我唯一使用== (或!= )的情况是:

 if (x != x) { // Here x is guaranteed to be Not a Number } 

我必须承认我有罪使用非数字作为魔术浮点常量(在C ++中使用numeric_limits<double>::quiet_NaN() )。

对于严格相等的浮点数进行比较没有意义。 浮点数的devise具有可预测的相对精度限制。 有责任知道他们和你的algorithm期望什么精度。

如果在比较之前,你永远不会计算出这个值,那可能就没问题了。 如果你正在testing,如果一个浮点数是正确的pi,或-1,或1,你知道这是有限的价值被传入…

当重写几个algorithm到multithreading版本时,我也用了几次。 我使用了一个testing,比较单线程和multithreading版本的结果,以确保它们给出完全相同的结果。

是。 1/x将有效,除非x==0 。 这里你不需要不精确的testing。 1/0.00000001是非常好的。 我想不出任何其他情况 – 你甚至不能检查tan(x)x==PI/2

比方说,你有一个函数,以一个常数因子来扩展浮点数组:

 void scale(float factor, float *vector, int extent) { int i; for (i = 0; i < extent; ++i) { vector[i] *= factor; } } 

我假设你的浮点实现可以完全代表1.0和0.0,而0.0代表全0位。

如果factor恰好是1.0,那么这个函数是一个没有操作,你可以返回而不做任何工作。 如果factor恰好为0.0,那么可以通过调用memset来实现,这可能比单独执行浮点乘法更快。

netlib中的BLAS函数的参考实现广泛地使用了这样的技术。

其他post显示适当的地方。 我认为使用位精确比较避免不必要的计算也是可以的。

例:

 float someFunction (float argument) { // I really want bit-exact comparison here! if (argument != lastargument) { lastargument = argument; cachedValue = very_expensive_calculation (argument); } return cachedValue; } 

我知道这是一个古老的线程,但我会说, 如果一个假阴性的答案是可以接受的 ,比较花车平等将是好的。

例如,假设您有一个将浮点数值打印到屏幕的程序,并且如果浮点值恰好等于M_PI ,那么您希望它打印出“pi”。 如果这个值恰好偏离了M_PI的精确的双重表示,它将会打印出一个double值,这个值同样有效,但是对于用户来说可读性要差一些。

在我看来,在大多数情况下比较平等(或一些等价)是一个需求:标准c ++容器或者带有隐含的相等比较函子的algorithm,比如std :: unordered_set,要求这个比较器是一个等价关系请参阅UnorderedAssociativeContainer 。 不幸的是,与abs(a - b) < epsilon ,由于失去传递性,不会产生等价关系。 这很可能是未定义的行为,特别是两个“几乎相等”的浮点数可能产生不同的哈希值; 这可以使unordered_set处于无效状态。 就个人而言,大部分时间我都会使用==来表示浮点数,除非任何一种fpu计算都会涉及任何操作数。 使用容器和容器algorithm,只涉及读/写,==(或任何等价关系)是最安全的。

abs(a - b) < epsilon或多或less是一个类似于极限的收敛标准。 如果我需要validation两个计算(例如PV = nRT,或距离=时间*速度)之间的math识别,我发现这个关系很有用。

总之,使用==当且仅当没有浮点计算发生; 从不使用abs(ab)<e作为等式谓词;

我有一个绘图程序,从根本上使用浮点坐标系统,因为用户可以在任何粒度/缩放工作。 他们正在绘制的东西包含可以在他们创build的点弯曲的线条。 当他们拖动一个点在另一个顶部时,他们被合并。

为了进行“适当的”浮点比较,我不得不想出一些范围来考虑相同的点。 由于用户可以放大到无穷大,并在该范围内工作,因为我不能让任何人承诺某种范围,我们只是使用'=='来查看点是否相同。 偶尔会有一个问题,应该是完全一样的点被closures.000000000001什么的(特别是在0,0左右),但通常它工作得很好。 不pipe怎样,打开这个button应该很难合并,或者至less这是原始版本的工作原理。

它偶尔会抛出testing小组,但这是他们的问题:p

所以无论如何,有一个可能合理的时间使用'=='的例子。 要注意的是,这个决定不是关于技术的准确性,而是关于客户的意愿(或缺乏)和方便。 不pipe怎么说都不需要那么精确 那么如果你期待他们两点不合并呢? 这不是世界末日,不会影响“计算”。