是浮点==还好吗?

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)

  • 公平地说,整数的保证和行为与任何其他价值观没有区别. (9认同)

Cam*_*ner 34

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

  1. 有没有float == float给出正确结果的情况?
  2. float == float可接受编码的情况吗?

(1)的答案是:是的,有时候.但它会变得脆弱,这导致了(2)的答案:不.不要这样做.你在将来乞求奇怪的错误.

对于表单的调用foo(BAR):在该特定情况下,比较将返回true,但是当您编写时,foo您不知道(并且不应该依赖)它的调用方式.例如,调用foo(BAR)会很好,但foo(BAR * 2.0 / 2.0)(或者甚至可能foo(BAR * 1.0)取决于编译器优化的东西)会破坏.你不应该依赖调用者不执行任何算术!

长话短说,尽管a == b在某些情况下你会真的不应该依赖它.即使你今天可以保证调用语义,也许你下周不能保证它们,所以省去一些痛苦但不要使用==.

在我看来,float == float永远不会好,因为它几乎不可维护.

*对于永远的小值.

  • 实际上与浮点相关的所有内容都非常标准,不太可能改变. (3认同)
  • @Alexandre:请记住,如果你正在处理NaN,你真的**不能使用==.根据定义,`NaN == <anything>`是假的,即使<anything>也是NaN.您需要使用`std :: isnan`来检查值是否为NaN. (3认同)
  • @Alexandre:我的意思是今天调用者使用`foo(BAR)`,但明天他们可能会把它改成`foo(BAR*1.0)` (2认同)
  • @Murrekatt:是的,我理解.在这种情况下,它应该全部**工作**好,但它依赖于良好的文档,以确保调用者使用正确的常量.这通常是一种相当脆弱的方法 - 很容易阅读或误读文档.即使它是由一个开发人员编写的,当你在六个月内回来并尝试维护代码时,它可能会导致混乱(充其量)和奇怪的错误(最坏的情况).当然,有时你需要讨厌的黑客,这可能是其中一种情况. (2认同)

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值存储在内存中时需要舍入.这意味着往返x8​​7 - > RAM - > x87会失去精度,因此计算结果会有所不同,具体取决于中间结果是通过RAM传递还是它们都保留在FPU寄存器中.这当然是编译器的决定,所以这个bug只能用于某些编译器和优化设置:-(.

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

相当吓人......

附加说明:

这种错误通常很难调试,因为一旦它们达到RAM,不同的值就会变得相同.

因此,例如,如果您将上述程序扩展为实际打印出位模式,yy2在比较它们之后,您将获得完全相同的值.要打印该值,必须将其加载到RAM中以传递给某些打印功能printf,这将使差异消失...


Dig*_*oss 8

即使在浮点格式中也非常适合积分值

但简短的回答是:"不,不要使用==."

具有讽刺意味的是,当在格式范围内的整数值上操作时,浮点格式"完美"地工作,即具有精确的精度.这意味着如果你坚持使用值,你可以获得略高于50位的完美整数,给你大约+ - 4,500,000,000,000,000,或4.5千万亿.

事实上,这是JavaScript的内部工作原理,这也是为什么JavaScript可以做这样的事情+,并-在真正的大数字,但只能<<>>32位的.

严格地说,您可以使用精确的表示来精确地比较数字的总和和产品.那些将是所有整数,加上由1/2 n项组成的分数.因此,增加n + 0.25,n + 0.50n + 0.75的循环可能没问题,但是没有任何其他96位小数的2位数.

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

  • 显然,它也是完美的,例如,可以表示为范围内的整数的值,除以2的幂.(稍微有点滑稽)的结论:换句话说,浮点数对于可以表示为浮点数的值是完美的. (4认同)

Ale*_* C. 7

我使用==(或!=)浮动的唯一情况如下:

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++中使用).

没有必要比较严格相等的浮点数.浮点数的设计具有可预测的相对精度限制.有责任了解对它们和算法的期望精度.

  • 只要操作不需要比可用的更精确,固定点就是100%精确.总是!浮点不遵循此规则.例如,只要不溢出固定数字的整数部分,就可以任意次数地添加或减去相同的精度定点数.第一次操作后浮动会有一些错误 - .- (2认同)

uli*_*tko 7

我将尝试为浮点平等提供或多或少的合法,有意义和有用的测试实例.

#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-81.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每一步都减半,而不是重新计算为(可能是指数中的巨大)ab.因此,如果我们将停止条件更改为d < eps,则算法将不会陷入具有巨大根的无限循环中(它很可能具有(b-a) < eps),但是在缩小d到低于精度的情况下仍将执行不必要的迭代a.

这种推理似乎过于理论化和不必要的深刻,但它的目的是再次说明花车的棘手.在围绕它们编写算术运算符时,应该非常小心它们的有限精度.