将浮点数与零进行比较

87

《C++ FAQ lite》"[29.17] 为什么我的浮点数比较不起作用?"建议使用以下等式测试:

#include <cmath>  /* for std::abs(double) */

inline bool isEqual(double x, double y)
{
  const double epsilon = /* some small number such as 1e-5 */;
  return std::abs(x - y) <= epsilon * std::abs(x);
  // see Knuth section 4.2.2 pages 217-218
}
  1. 这是否意味着,只有 +0-0 这两个数字等于零?
  2. 在测试是否为零时,应该使用这个函数还是像 |x| < epsilon 这样的测试?

更新

正如 Daniel Daranas 指出的那样,这个函数最好被称为 isNearlyEqual(这是我关心的情况)。

有人指出了“比较浮点数”,我想更加突出地分享一下。


4
我脑海中有一句话,它说不要比较两个浮点数是否相等,只需要比较它们的大小关系。请注意保持原意,使翻译更加通俗易懂。 - user743414
10
在某些情况下,测试一个double是否相等是完全可以的。例如:if(counter > 10.0) { counter = 0.0; //dostuff } 在代码的其他地方:if(counter == 0.0){//oh I know that counter is reseted} else{//do other stuff}... - relaxxx
你实际上想做什么?至于问题1,是的,唯一与+0.0(或-0.0)相等的值是+0.0和-0.0。但我没有看到问题中的代码暗示了这一点。 - David Heffernan
2
@relaxxx: 计数器是整数。 - n. m.
2
可能是我们是否应该针对*相对*误差比较浮点数的相等性?的重复问题。 - Ivan Aksamentov - Drop
显示剩余8条评论
9个回答

57
您的观察是正确的。
如果x == 0.0,那么abs(x) * epsilon就是零,您正在测试abs(y) <= 0.0
如果y == 0.0,那么您正在测试abs(x) <= abs(x) * epsilon,这意味着epsilon >= 1(它不是),或者x == 0.0
因此,要么is_equal(val, 0.0)is_equal(0.0, val) 是没有意义的,您可以直接说val == 0.0。如果您只想接受恰好+0.0-0.0
在这种情况下,FAQ的建议有限。在浮点数比较中没有一种方法适用于所有情况。您必须考虑变量的语义、可接受的值范围以及计算引入的误差的大小。即使FAQ也提到了一个警告,说这个函数通常不会出现问题,“当x和y的数量级显着大于epsilon时”。

21
如果您只对+0.0-0.0感兴趣,可以使用<cmath>中的fpclassify。例如: if( FP_ZERO == fpclassify(x) ) do_something;

方便模板中不知道浮点数(或整数)类型的情况 - 它适用于整数类型。 - mheyman

20

不。

平等就是平等。

你写的函数并不会像它的名字所承诺的那样测试两个双精度浮点数是否相等。它只会测试两个双精度浮点数是否“足够接近”。

如果你真的想测试两个双精度浮点数是否相等,请使用这个函数:

inline bool isEqual(double x, double y)
{
   return x == y;
}

编码标准通常不建议将两个double精确比较相等。但那是一个不同的主题。 如果您实际上想要比较两个double是否精确相等,x == y是您想要的代码。

10.000000000000001不等于10.0,无论别人告诉你什么。

一个示例使用精确相等的情况是当double的特定值被用作某些特殊状态的代名词,例如“待定计算”或“无可用数据”。只有在挂起计算后实际数值只是double可能值的子集时才可能这样做。最典型的情况是该值为非负数,并且您使用-1.0作为“待处理计算”或“无可用数据”的(精确)表示。您可以使用常量来表示:

const double NO_DATA = -1.0;

double myData = getSomeDataWhichIsAlwaysNonNegative(someParameters);

if (myData != NO_DATA)
{
    ...
}

23
你的isEqual函数总有一天会让你惊讶 :) 或许是当你意识到浮点数有多精确时。 - BЈовић
26
@BЈовић 不会让我感到惊讶。我很少使用它,但是当我使用时,我会意味着要检查一个双精度浮点数是否等于另一个。 10.00000000000000001不等于10.0。 - Daniel Daranas
8
我认为问题的根源在于人们使用了浮点精度,而实际上他们想要的是固定的精度。丹尼尔的论点(10.00000000000000001不是10.0)很有道理,但人们在比较“5.0 + 5.0”和“15.0 - 5.0”时,并没有预料到这一点可能会产生影响。 - kfsone
5
但丹尼尔的论点有问题,因为当我将10.00000000000000001存储为浮点数时,它也不是完全的10.00000000000000001。浮点数并不是精确的,它们只是近似值。 - DaveB
12
这并不与我的观点相矛盾。我的观点是,相等的值就是相等的,如果你想知道两个浮点数是否相等,就要进行相等性测试。如果你对此不感兴趣,那就不要测试它们的相等性——而是测试它们是否足够接近。 - Daniel Daranas
显示剩余17条评论

6

您可以使用std::nextafter,并固定一个epsilonfactor来处理如下值:

bool isNearlyEqual(double a, double b)
{
  int factor = /* a fixed factor of epsilon */;

  double min_a = a - (a - std::nextafter(a, std::numeric_limits<double>::lowest())) * factor;
  double max_a = a + (std::nextafter(a, std::numeric_limits<double>::max()) - a) * factor;

  return min_a <= b && max_a >= b;
}

4

2 + 2 = 5(*)

(对于某些浮点精度值的2)

当我们将“浮点数”视为增加精度的方法时,这个问题经常出现。然后我们遇到了“浮动”部分,这意味着没有保证可以表示哪些数字。

因此,虽然我们可能很容易地表示“1.0,-1.0,0.1,-0.1”,但随着数字变大,我们开始看到近似值 - 或者我们应该看到,除非我们通常通过截断数字来隐藏它们以供显示。

因此,我们可能认为计算机正在存储“0.003”,但它实际上可能存储“0.0033333333334”。

如果执行“0.0003 - 0.0002”会发生什么?我们期望得到0.0001,但实际存储的值可能更接近于“0.00033”-“0.00029”,这产生了“0.000004”,或者是最接近可表示值,可能是0,也可能是“0.000006”。

当前浮点数运算中, (a / b) * b == a 不能保证成立。

#include <stdio.h>

// defeat inline optimizations of 'a / b * b' to 'a'
extern double bodge(int base, int divisor) {
    return static_cast<double>(base) / static_cast<double>(divisor);
}

int main() {
    int errors = 0;
    for (int b = 1; b < 100; ++b) {
        for (int d = 1; d < 100; ++d) {
            // b / d * d ... should == b
            double res = bodge(b, d) * static_cast<double>(d);
            // but it doesn't always
            if (res != static_cast<double>(b))
                ++errors;
        }
    }
    printf("errors: %d\n", errors);
}

ideone报告了599个实例,其中(b * d) / d != b,仅使用1<=b<=100和1<=d<=100的10,000种组合。

FAQ中描述的解决方案基本上是应用粒度约束 - 测试if (a == b +/- epsilon)

另一种方法是通过使用定点精度或将所需粒度用作存储的基本单位来完全避免问题。例如,如果您希望以纳秒精度存储时间,请使用纳秒作为存储单位。

C++11引入了std::ratio作为不同时间单位之间的定点转换的基础。


1

正如@Exceptyon所指出的那样,这个函数是与您比较的值“相关”的。 Epsilon * abs(x)度量将根据x的值进行缩放,以便无论x或y中的值的范围如何,您都将获得与epsilon一样准确的比较结果。

如果您将零(y)与另一个非常小的值(x)进行比较,例如1e-8,则abs(x-y)= 1e-8仍将远大于epsilon * abs(x)= 1e-13 。因此,除非您处理不能用double类型表示的极小数,否则该函数应该可以胜任,并且只会将零与+0-0匹配。

该函数对于零比较似乎完全有效。如果您计划使用它,请在涉及到浮点数的所有地方使用它,而不是针对零等特殊情况,以便代码统一。

附言:这是一个很棒的函数。感谢指向它。


1
考虑以下例子:
bool isEqual = (23.42f == 23.42);

isEqual 是什么? 有9个人会说 "当然是true",但这9个人都是错的:https://rextester.com/RVL15906

这是因为浮点数不是精确的数字表示形式。

作为二进制数,它们甚至不能准确地表示所有可以用十进制数精确表示的数字。例如,虽然 0.1 可以被精确表示为十进制数(它恰好是 1 的十分之一),但在使用浮点数时无法表示,因为它在二进制中是周期性的 0.00011001100110011...。 对于浮点数来说,0.1 就像对于十进制数的 1/3 一样(在十进制中是 0.33333...)。

结果是,像 0.3 + 0.6 这样的计算可能会得出 0.89999999999999991,虽然很接近但并不等于 0.9。因此,测试 0.1 + 0.2 - 0.3 == 0.0 可能会失败,因为计算结果可能不是 0,尽管它非常接近于 0== 是一种精确测试,在不精确的数字上进行精确测试通常没有太多意义。由于许多浮点计算包括舍入误差,因此您通常希望您的比较也允许小误差,这就是您发布的测试代码的目的。它不是测试“A是否等于B”,而是测试“A是否非常接近B”,因为非常接近通常是您可以从浮点计算中期望的最佳结果。

1

对于FP(浮点数)的简单比较有其自身的特定,关键在于理解FP格式(请参见https://en.wikipedia.org/wiki/IEEE_floating_point)。

当以不同的方式计算FP数字时,一个通过sin(),另一个通过exp(),即使在数学上数字相等,严格的相等也不起作用。同样的,使用常量进行比较也不起作用。实际上,在许多情况下,不能使用严格的相等(==)来比较FP数字。

在这种情况下,应该使用DBL_EPSILON常量,它是最小值不要更改,表示将1.0添加到数字中超过1.0的部分。对于大于2.0的浮点数,根本不存在DBL_EPSILON。与此同时,DBL_EPSILON的指数为-16,这意味着所有指数为-34的数字与DBL_EPSILON相比将完全相等。

另请参见example,为什么10.0 == 10.0000000000000001。

比较两个浮点数取决于它们的性质,我们应该为它们计算DBL_EPSILON以便进行有意义的比较。简单来说,我们应该将DBL_EPSILON乘以其中一个数字。哪一个?当然是最大值。

bool close_enough(double a, double b){
    if (fabs(a - b) <= DBL_EPSILON * std::fmax(fabs(a), fabs(b)))
    {
        return true;
    }
    return false;
}

所有其他方式都可能导致不等式错误,这些错误可能非常难以捕获。

不添加是因为整数部分使用了尾数。10.00000000000000001无法被double表示。 - Volodymyr Boiko
1
浮点表达式没问题,但我看不出为什么它不能直接返回测试表达式的布尔值,而不是将其放在某个 if 语句中并返回 True 或 False。当然,编译器会对此进行优化,但没有必要使代码比需要的更冗长。 - kriss

0

请注意,这段代码是:

std::abs((x - y)/x) <= epsilon

你要求方差的"相对误差"小于等于epsilon,而不是绝对差


6
如果x不等于0,那么这个表达式才是相等的。 - Daniel Daranas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接