是浮点==还好吗?

时间:2011-01-13 17:05:36

标签: c++ comparison floating-point

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

// 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

14 个答案:

答案 0 :(得分:37)

是的,保证整数,包括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 !!

答案 1 :(得分:34)

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

  1. 是否存在float == float给出正确结果的情况?
  2. 是否存在float == float可接受编码的情况?
  3. (1)的答案是:是的,有时候。但它会变得脆弱,这导致了(2)的答案:不。不要这样做。你将来会乞求奇怪的错误。

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

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

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

    *适用于从不小的值。

答案 2 :(得分:14)

其他答案很好地解释了为什么将==用于浮点数是危险的。我相信,我刚刚发现了一个很好地说明这些危险的例子。

在x86平台上,由于您执行的计算固有的舍入问题,您可以获得某些计算的奇怪浮点结果,这些结果 not 。这个简单的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;

(仅分布在两个函数和中间变量上),但比较仍然会产生错误!

原因是在x86平台上,程序通常使用x87 FPU进行浮点计算。 x87内部计算的精度高于常规double,因此double值在存储在内存中时需要舍入。这意味着往返x8​​7 - &gt; RAM - &gt; x87失去精度,因此计算结果根据中间结果是否通过RAM传递或者它们是否都保留在FPU寄存器中而有所不同。这当然是编译器的决定,所以这个bug只能用于某些编译器和优化设置: - (。

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

相当可怕......

附加说明:

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

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

答案 3 :(得分:8)

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

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

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

事实上,这就是JavaScript内部工作的原因,这就是为什么JavaScript可以在真正大数字上执行+-之类的操作,但只能<<和{{1}在32位的。

严格地说,您可以使用精确的表示来精确地比较数字的总和和乘积。那些将是所有整数,加上由 1/2 n 术语组成的分数。因此,通过 n + 0.25,n + 0.50, n + 0.75 递增的循环将是正常的,但不是任何其他具有2位数的96个小数部分。< / p>

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

答案 4 :(得分:7)

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

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

我必须承认我使用Not A Number作为魔术浮点常量(在C ++中使用numeric_limits<double>::quiet_NaN())。

对于严格相等的浮点数进行比较没有意义。浮点数的设计具有可预测的相对精度限制。 负责了解对它们和算法的期望精度。

答案 5 :(得分: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));
}

我宁愿不解释bisection method使用过的本身,而是强调停止条件。它具有完全讨论的形式:(a == a+d)其中双方都是浮点数:a是我们当前近似的方程根,d是我们当前的精度。鉴于算法的前提条件 - 必须range_startrange_end之间的根 - 我们保证在根之间的每次迭代之间aa+d,而d在每一步都减半,缩小范围。

然后,经过多次迭代后,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 - 小的加数消失,四舍五入为零。在我们的算法的某个迭代中完全相同的保证 - 并且在那时我们不能再继续了。我们找到了最大可能精度的根。

显然,令人启发的结论是花车很棘手。它们看起来非常像真正的数字,每个程序员都试图将它们视为实数。但他们不是。他们有自己的行为,略微让人想起真正的,但不完全一样。你需要非常小心它们,特别是在比较相等时。


更新

一段时间后重新回答答案,我也注意到一个有趣的事实:在上面的算法中,一个不能在停止条件下实际使用“some small number”。对于任何数字的选择,会有输入会认为您的选择太大,导致精度损失,会有输入会认为您的选择太小,导致过多的迭代甚至进入无限循环。详细讨论如下。

你可能已经知道微积分没有“小数字”的概念:对于任何实数,你可以很容易地找到无数甚至更小的数。问题在于,其中一个“更小”的可能是我们实际寻求的东西;它可能是我们等式的根源。更糟糕的是,对于不同的方程式,可能存在不同的根(例如2.51e-81.38e-8),两者将由近似如果我们的停止条件看起来像d < 1e-6,则相同的数字。无论你选择哪个“小数字”,很多根据a == a+d停止条件被正确找到的根将被“epsilon”太大所破坏。

然而,在浮点数中,指数具有有限的范围,因此您实际上可以找到最小的非零正FP数(例如1e-45 denorm for IEEE 754 single precision FP) 。但它没用! while (d < 1e-45) {...}将永远循环,假设是单精度(正非零)d

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

我上面的实现也考虑了最后一个问题,你可以看到d每一步减半,而不是重新计算为(可能是指数中的巨大)a和{{ {1}}。因此,如果我们将停止条件更改为b,则算法将不会陷入具有巨大根的无限循环(很可能与d < eps),但在收缩期间仍将执行不必要的迭代{{ 1}}低于(b-a) < eps的精度。

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

答案 6 :(得分:4)

如果您在比较它之前永远不会计算该值,那可能没问题。如果您正在测试浮点数是否正好是pi,或-1或1,并且您知道传入的限制值是...

答案 7 :(得分:2)

在将多个算法重写为多线程版本时,我也使用了几次。我使用了一个测试来比较单线程和多线程版本的结果,以确保它们都能给出完全相同的结果。

答案 8 :(得分:2)

在我看来,在大多数情况下需要比较相等(或某些等价):标准C ++容器或带有隐含等式比较算子的算法,例如std :: unordered_set,要求此比较器为等价关系(见 C++ named requirements: UnorderedAssociativeContainer )。

不幸的是,与abs(a - b) < epsilon中的epsilon相比,它不会产生等价关系,因为它会失去传递性。这很可能是未定义的行为,特别是两个几乎相同的行为。浮点数可以产生不同的哈希值;这可以将unordered_set置于无效状态。 就个人而言,我会在大多数时间使用==浮点数,除非任何操作数都涉及任何类型的FPU计算。对于容器和容器算法,只涉及读/写,==(或任何等价关系)是最安全的。

abs(a - b) < epsilon或多或少是一个类似于限制的收敛标准。如果我需要验证两次计算之间是否存在数学同一性(例如PV = nRT,或者距离=时间*速度),我发现这种关系很有用。

简而言之,当且仅当没有浮点计算发生时才使用==; 永远不要使用abs(a-b) < e作为等式谓词;

答案 9 :(得分:1)

是。除非1/x,否则x==0将有效。你不需要在这里进行不精确的测试。 1/0.00000001完全没问题。我想不出任何其他情况 - 您甚至无法检查tan(x) <{1}}

答案 10 :(得分:1)

假设您有一个函数可以通过常数因子来缩放浮点数组:

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来实现,这可能比单独执行浮点乘法更快。

reference implementation of BLAS functions at netlib广泛使用此类技术。

答案 11 :(得分:1)

其他帖子显示适当的位置。我认为使用比特精确比较来避免不必要的计算也没问题。

示例:

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

  return cachedValue;
}

答案 12 :(得分:0)

我想说,如果可以接受假阴性答案,那么比较浮点数是否正确

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

答案 13 :(得分:-3)

我有一个绘图程序,从根本上为其坐标系使用浮点,因为允许用户以任何粒度/缩放工作。他们绘制的东西包含可以在它们创建的点处弯曲的线条。当他们将一个点拖到另一个点之上时,它们就会被合并。

为了进行“正确的”浮点比较,我必须提出一些范围来考虑相同的点。由于用户可以放大到无穷大并在该范围内工作,并且由于我无法让任何人承诺某种范围,我们只需使用'=='来查看这些点是否相同。偶尔会出现一个问题,即应该完全相同的点是.000000000001或其他东西(特别是0,0左右),但通常情况下它可以正常工作。如果没有打开快照,它应该很难合并点......或者至少原始版本是如何工作的。

它会偶尔抛出测试组,但那是他们的问题:p

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