mur*_*att 52 c++ floating-point comparison
就在今天,我遇到了我们正在使用的第三方软件,在他们的示例代码中,有以下几点:
// Defined in somewhere.h
static const double BAR = 3.14;
// Code elsewhere.cpp
void foo(double d)
{
if (d == BAR)
...
}
Run Code Online (Sandbox Code Playgroud)
我知道浮点数及其表示的问题,但它让我想知道是否有float == float可能会好的情况?我不是在问它什么时候可行,而是在它有意义和有效的时候.
还有,电话foo(BAR)怎么样?它总是比较相同,因为它们都使用相同的static const BAR吗?
Mar*_*ett 37
是的,您可以保证整数(包括0.0)与==相比较
当然,你必须要小心一点,如何获得整数,分配是安全的,但任何计算的结果都是可疑的
ps有一组实数作为浮点数有一个完美的再现(想想1/2,1/4 1/8等),但你可能事先并不知道你有其中之一.
只是为了澄清.IEEE 754保证在范围内浮点表示整数(整数)是精确的.
float a=1.0;
float b=1.0;
a==b // true
Run Code Online (Sandbox Code Playgroud)
但你必须要小心如何获得整数
float a=1.0/3.0;
a*3.0 == 1.0 // not true !!
Run Code Online (Sandbox Code Playgroud)
Cam*_*ner 34
有两种方法可以回答这个问题:
float == float给出正确结果的情况?float == float可接受编码的情况吗?(1)的答案是:是的,有时候.但它会变得脆弱,这导致了(2)的答案:不.不要这样做.你在将来乞求奇怪的错误.
对于表单的调用foo(BAR):在该特定情况下,比较将返回true,但是当您编写时,foo您不知道(并且不应该依赖)它的调用方式.例如,调用foo(BAR)会很好,但foo(BAR * 2.0 / 2.0)(或者甚至可能foo(BAR * 1.0)取决于编译器优化的东西)会破坏.你不应该依赖调用者不执行任何算术!
长话短说,尽管a == b在某些情况下你会真的不应该依赖它.即使你今天可以保证调用语义,也许你下周不能保证它们,所以省去一些痛苦但不要使用==.
在我看来,float == float永远不会好,因为它几乎不可维护.
*对于永远的小值.
sle*_*ske 14
其他答案很好地解释了为什么使用==浮点数是危险的.我相信,我刚刚找到一个可以很好地说明这些危险的例子.
在x86平台上,您可以获得一些计算的奇怪浮点结果,这不是由于您执行的计算固有的舍入问题.这个简单的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);
}
Run Code Online (Sandbox Code Playgroud)
该程序基本上只是计算
x = 0.012 + 1.0;
y = 0.012 + 1.0;
Run Code Online (Sandbox Code Playgroud)
(只分布在两个函数和中间变量上),但比较仍然会产生错误!
原因是在x86平台上,程序通常使用x87 FPU进行浮点计算.x87内部计算的精度高于常规值double,因此double值存储在内存中时需要舍入.这意味着往返x87 - > RAM - > x87会失去精度,因此计算结果会有所不同,具体取决于中间结果是通过RAM传递还是它们都保留在FPU寄存器中.这当然是编译器的决定,所以这个bug只能用于某些编译器和优化设置:-(.
有关详细信息,请参阅GCC错误:http://gcc.gnu.org/bugzilla/show_bug.cgi?id = 323
相当吓人......
附加说明:
这种错误通常很难调试,因为一旦它们达到RAM,不同的值就会变得相同.
因此,例如,如果您将上述程序扩展为实际打印出位模式,y并y2在比较它们之后,您将获得完全相同的值.要打印该值,必须将其加载到RAM中以传递给某些打印功能printf,这将使差异消失...
但简短的回答是:"不,不要使用==."
具有讽刺意味的是,当在格式范围内的整数值上操作时,浮点格式"完美"地工作,即具有精确的精度.这意味着如果你坚持使用双值,你可以获得略高于50位的完美整数,给你大约+ - 4,500,000,000,000,000,或4.5千万亿.
事实上,这是JavaScript的内部工作原理,这也是为什么JavaScript可以做这样的事情+,并-在真正的大数字,但只能<<和>>32位的.
严格地说,您可以使用精确的表示来精确地比较数字的总和和产品.那些将是所有整数,加上由1/2 n项组成的分数.因此,增加n + 0.25,n + 0.50或n + 0.75的循环可能没问题,但是没有任何其他96位小数的2位数.
所以答案是:虽然在狭义的情况下,确切的平等在理论上是有意义的,但最好避免.
我使用==(或!=)浮动的唯一情况如下:
if (x != x)
{
// Here x is guaranteed to be Not a Number
}
Run Code Online (Sandbox Code Playgroud)
我必须承认我使用Not A Number作为一个神奇的浮点常数(numeric_limits<double>::quiet_NaN()在C++中使用).
没有必要比较严格相等的浮点数.浮点数的设计具有可预测的相对精度限制.您有责任了解对它们和算法的期望精度.
我将尝试为浮点平等提供或多或少的合法,有意义和有用的测试实例.
#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));
}
Run Code Online (Sandbox Code Playgroud)
我宁愿不解释自己使用的二分法,而是强调停止条件.它具有完全讨论的形式:(a == a+d)两边都是浮点数:a是我们当前近似的方程根,d是我们当前的精度.考虑到算法的前提条件 - 在和之间必须有一个根- 我们保证在每次迭代时根保持在每个步骤之间,并且while 在每一步减半,缩小边界.range_startrange_endaa+dd
然后,经过多次迭代后,d变得非常小,以至于在添加过程中a它会变为零!也就是说,a+d原来是接近到a然后到任何其他浮动 ; 因此FPU将其舍入到最接近的值:a自身.这可以通过在假设的计算机上计算来容易地说明; 让它有4位十进制尾数和一些大的指数范围.那么机器应该给出什么结果2.131e+02 + 7.000e-3?确切的答案是213.107,但我们的机器不能代表这样的数字; 它必须围绕它.而213.107更接近213.1,而不是213.2-所以圆形的结果变成2.131e+02-小加数消失,四舍五入为零.在我们的算法的某些迭代中保证完全相同- 并且在那时我们不能再继续.我们找到了最大可能精度的根.
显然,令人启发的结论是花车很棘手.它们看起来非常像真实数字,每个程序员都倾向于将它们视为实数.但他们不是.他们有自己的行为,略微让人想起真实的行为,但并不完全相同.你需要非常小心它们,特别是在比较平等时.
一段时间后重新回答答案,我也注意到一个有趣的事实:在上面的算法中,在停止条件下实际上不能使用"一些小数字".对于数字的任何选择,会有输入会使您的选择太大,导致精度损失,并且会有输入会使您认为选择太小,导致过多的迭代甚至进入无限循环.详细讨论如下.
您可能已经知道,微积分没有"小数"的概念:对于任何实数,您可以轻松找到无数甚至更小的数.问题在于,其中一个"更小"的可能是我们实际寻求的东西; 它可能是我们等式的根源.更糟糕的是,对于不同的方程,可能存在不同的根(例如2.51e-8和1.38e-8),如果我们的停止条件看起来,两者都将被相同的数字近似d < 1e-6.无论你选择哪个"小数字" a == a+d,在"ε" 太大的情况下,许多根据停止条件被正确找到的根将被破坏.
然而,在浮点数中,指数具有有限的范围,因此您实际上可以找到最小的非零正FP数(例如1e-45,IEEE 754单精度FP的变形).但它没用!while (d < 1e-45) {...}假设单精度(正非零),将永远循环d.
抛开那些病态边缘情况,停止条件下任何 "小数"的选择对于许多方程来说都太小了.在根具有足够高的指数的那些方程中,两个尾数的减法结果仅在最低有效数字处不同将很容易超过我们的"epsilon".例如,对于6位尾数,意味着指数+8和5位尾数的数字之间的最小可能差异是...... 1000!例如,哪个永远不适合.对于具有(相对)高指数的这些数字,我们根本没有足够的精度来看到差异.d < eps7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 10001e-41e-4
我上面的实现也考虑了最后一个问题,你可以看到d每一步都减半,而不是重新计算为(可能是指数中的巨大)a和b.因此,如果我们将停止条件更改为d < eps,则算法将不会陷入具有巨大根的无限循环中(它很可能具有(b-a) < eps),但是在缩小d到低于精度的情况下仍将执行不必要的迭代a.
这种推理似乎过于理论化和不必要的深刻,但它的目的是再次说明花车的棘手.在围绕它们编写算术运算符时,应该非常小心它们的有限精度.