浮点数/双精度浮点数的精确零比较

9

我有一个算法,它使用浮点数双精度浮点数进行一些计算。

例如:

double a;
double b;
double c;
...
double result = c / (b - a);
if ((result > 0) && (result < small_number))
{
    // result is relevant...
} else {
    // result not required...
}

现在,我担心(b - a)可能为零。如果它接近于零但不等于零,那并不重要,因为result将超出可用范围,并且我已经检测到这一点(随着(b - a)趋近于零,result将趋向于+/-无穷大,这不在范围内 0-small_number...)。
但是,如果(b - a)的结果恰好为零,我预计由于除以零会发生一些与平台相关的情况。我可以将if语句更改为:
if ((!((b-a) == 0.0)) && ((result = c/(b-a)) > 0) && (result < small_number)) {

但我不知道 (b-a) == 0.0 是否总是能检测到和零相等。我发现浮点数中有多种表示确切的零?如何测试它们,而不需要执行某些 epsilon 检查(在我的算法中不需要小 epsilon)?
有什么平台无关的方法可以检查?
编辑:
不确定对人们来说是否清晰明了。基本上我想知道如何找出像这样的表达式是否相等:
double result = numerator / denominator;

如果不进行实际操作而只是依靠检测,可能会导致浮点异常、CPU异常、操作系统信号或其他异常。因为检测这种情况的"抛出"似乎很复杂并且与平台有关。

( (denominator==0.0) || (denominator==-0.0) ) ? "会'抛出'" : "不会'抛出'"; 这样的判断足够吗?


1
即使 b - a 不完全等于零,操作 c / (b - a) 仍可能溢出并将值发送到 +/-INF - Mysticial
8
很难找到不使用IEEE浮点数的实现。所以我认为可以安全地假定除以0将产生+INF-INF?如果我错了,有人能纠正我吗? - Mysticial
2
@Mysticial:这取决于分子是什么。显然,0.0 / 0.0 是NaN,并且在启用异常时会触发不同的FP异常。 - cHao
顺便说一句,Mystical部分正确。GCC只会产生一个分段错误,这是一个无法捕获的异常(我认为)。无论如何,除以零不应该成为一个被捕获的异常。我认为这是一个很好的问题,在我看来,任何编写除法运算符的人都应该始终测试是否为零,而不管代码如何。 - eggmatters
2
尝试重新排列条件,使您除以small_number而不是b-a - Mark Ransom
显示剩余2条评论
8个回答

10

这取决于ba如何获取它们的值。零在浮点格式中具有精确表示,但更大的问题是几乎为零的值。始终检查是安全的:

if (abs(b-a) > 0.00000001  && ...

其中0.00000001是任何有意义的值。


5
为什么一个接近但不完全为零的数会成为问题呢?我的算法已经排除了接近但不完全为零的分母的结果... - Bingo
@Bingo:我没有看到你的算法拒绝值为1e-300b-a。这样的测试成本很小,对于抵抗代码变形来说是非常好的保险,即使其中一个版本无法产生该值。 - wallyk
1
问题在于,任何数除以接近于零的数都会变得非常大……这超出了我感兴趣的 0small_number 范围。 - Bingo
1
应该使用fabs而不是abs吧? - daisy
@warl0ck:如果你想使用函数版本,那也可以。然而,有一个宏/模板版本的abs()适用于所有标量类型。 - wallyk
2
这个答案在我的搜索中帮了我很多。一开始对于 abs vs. fabs 持怀疑态度,在 gcc 中,abs 适用于所有标量类型(我也在 qreal 和 glFloat 上测试过)。我的建议是使用三元运算符来内联测试(只要你是维护者或者你有很多注释)double result = c / ((abs((b - a)) > 0.00000001f) ? (b - a) : 0.00000001f) ; 然而,这个表达式非常丑陋。 - eggmatters

6

以下是如何操作:不要检查(result < small_number),而是检查

(abs(c) < abs(b - a) * small_number)

然后你所有的麻烦都会消失!只要通过此测试,c/(b-a)的计算就永远不会溢出。


2
简而言之,我们只有知道浮点数的表示格式,才能确切地知道它是否为零。
实际上,我们将x与一个小数进行比较。如果x小于这个数,我们就认为x在功能上与零相同(但大多数情况下,我们的小数仍然大于零)。这种方法非常简单、高效,并且可以跨平台使用。
实际上,浮点数和双精度数已经被以特殊格式呈现,目前硬件中广泛使用的是 IEEE 754,它将数字分成符号、指数和尾数(有效数字)位。
因此,如果我们想要确切地检查浮点数是否为,我们可以检查指数和尾数是否都为零,参见这里
在IEEE 754二进制浮点数中,零值由偏置指数和尾数均为零表示。负零的符号位设置为1。
float为例,我们可以编写一个简单的代码来提取指数和尾数位,然后进行检查。
#include <stdio.h>   

typedef union {
          float f;
          struct {
              unsigned int mantissa : 23;
              unsigned int exponent : 8;
              unsigned int sign :     1;
          } parts;
} float_cast;


int isZero(float num) {

    int flag = 0;
    float_cast data;
    data.f = num;

    // Check both exponent and mantissa parts
    if(data.parts.exponent == 0u && data.parts.mantissa == 0u) {
       flag = 1;
    } else {
       flag = 0;
   }

   return(flag);
}


int main() {

   float num1 = 0.f, num2 = -0.f, num3 = 1.2f;

   printf("\n is zero of %f -> %d", num1, isZero(num1));
   printf("\n is zero of %f -> %d", num2, isZero(num2));
   printf("\n is zero of %f -> %d", num3, isZero(num3));

   return(0);
}

测试结果: ``` # 是 0 的零点为 0.000000 -> 1 # 是 -0 的零点为 -0.000000 -> 1 # 是 1.200000 的零点为 -> 0 ```
更多例子:
让我们用代码检查浮点数何时变为实际的零。
void test() {
     int i =0;
     float e = 1.f, small = 1.f;
     for(i = 0; i < 40; i++) {
         e *= 10.f;
         small = 1.f/e;
         printf("\nis %e zero? : %d", small, isZero(small));
     }
     return;
}


is 1.0000e-01 zero? : NO
is 1.0000e-02 zero? : NO
is 1.0000e-03 zero? : NO
is 1.0000e-04 zero? : NO
is 1.0000e-05 zero? : NO
is 1.0000e-06 zero? : NO
is 1.0000e-07 zero? : NO
is 1.0000e-08 zero? : NO
is 1.0000e-09 zero? : NO
is 1.0000e-10 zero? : NO
is 1.0000e-11 zero? : NO
is 1.0000e-12 zero? : NO
is 1.0000e-13 zero? : NO
is 1.0000e-14 zero? : NO
is 1.0000e-15 zero? : NO
is 1.0000e-16 zero? : NO
is 1.0000e-17 zero? : NO
is 1.0000e-18 zero? : NO
is 1.0000e-19 zero? : NO
is 1.0000e-20 zero? : NO
is 1.0000e-21 zero? : NO
is 1.0000e-22 zero? : NO
is 1.0000e-23 zero? : NO
is 1.0000e-24 zero? : NO
is 1.0000e-25 zero? : NO
is 1.0000e-26 zero? : NO
is 1.0000e-27 zero? : NO
is 1.0000e-28 zero? : NO
is 1.0000e-29 zero? : NO
is 1.0000e-30 zero? : NO
is 1.0000e-31 zero? : NO
is 1.0000e-32 zero? : NO
is 1.0000e-33 zero? : NO
is 1.0000e-34 zero? : NO
is 1.0000e-35 zero? : NO
is 1.0000e-36 zero? : NO
is 1.0000e-37 zero? : NO
is 1.0000e-38 zero? : NO
is 0.0000e+00 zero? : YES   <-- 1e-39
is 0.0000e+00 zero? : YES   <-- 1e-40

2

我猜你可以使用fpclassify(-0.0) == FP_ZERO。但是,这只有在您想检查是否将某种类型的零放入浮点变量时才有用。正如许多人已经说过的那样,如果您想检查计算结果,由于表示的本质,您可能会得到非常接近零的值。


请在给我点踩时留下原因,我保证这只是为了理解我的错误。 - ony

1

更新(2016年01月04日)

我在这个答案上收到了一些负评,我想知道是否应该将其删除。似乎共识(https://meta.stackexchange.com/questions/146403/should-i-delete-my-answers)是,仅在极端情况下才应删除答案。

所以,我的答案是错误的。但我想我会把它保留下来,因为它提供了一个有趣的“超越常规思维”的思考实验。

===============

太好了,

你说你想知道b-a是否等于0。

另一种看待这个问题的方式是确定a是否等于b。如果a等于b,那么b-a将等于0。

我发现另一个有趣的想法:

http://www.cygnus-software.com/papers/comparingfloats/Comparing%20floating%20point%20numbers.htm

基本上,您需要将您拥有的浮点变量告诉编译器重新解释它们(按位)为有符号整数,如下所示:

if (*(int*)&b == *(int*)&a)

那么你正在比较整数,而不是浮点数。也许这会有所帮助?也许不会。祝你好运!


2
如果没有那个相当丑陋的强制转换,我会点赞这个的。在这种情况下,if (a!=b) { /* do the math */ }已经足够好了;其他任何东西都是过度设计。 - cHao
谢谢你的回答。这个等式的想法值得思考。如果分母不是减法的结果,更一般的解决方案会很好。此外,我认为比较位(即整数方法)在这里是问题所在。不同的零表示方式将具有不同的位模式。 - Bingo
3
不仅*(int*)&b == *(int*)&a违反了严格别名规则,而且它还会使得+0.-0.看起来不同。我不认为在这里使用这种比较有任何意义。 - Pascal Cuoq
1
更糟糕的是,在几乎所有平台上,int 是32位,而 double 是64位,因此您只是比较高位或低位(取决于您的平台)的32位是否相等,这是无用的。(另外,如果两个NaN恰好具有相同的位设置,则会将它们视为相等,如果它们没有,则不相等,这绝对是没有用的...) - abarnert

0

我相信当(b-a)==0时,c/(b-a)将会失败,因为(b-a)为零。浮点数运算很棘手,但我认为质疑这一点是夸大其词的。此外,我相信(b-a)==0将等同于b!=a

区分正零和负零也不是必要的。例如,在这里看看浮点数是否有负零?(-0f)


关于“C++中子表达式的评估顺序未定义”的问题,在 e1 && e2 中,评估顺序是已定义的。只有在 e1 评估为 true/非零时,才会对 e2 进行评估,并且此时 e2 总是在 e1 之后评估。 - Pascal Cuoq
你说得对!https://dev59.com/v2w05IYBdhLWcg3wpTYV 我会删除那部分内容。虽然被删除的那一部分是我回复的原因,但其他部分我还是会保留。 - David L.
如果编译器遵循IEEE标准,使用浮点零进行除法是被定义好的。请在SO上阅读更多信息或查看IEEE浮点数标准 - DanielTuzes

0

对于epsilon,在C++中有一个标准的模板定义std::numeric_limits::epsilon()。我猜检查差异是否大于std::numeric_limits::epsilon()应该足够安全,以防止除以零。我想这里没有平台依赖。


这个回答是无意义的。std::numeric_limits::epsilon() 不适用于此用途,它测量略大于 1.0 的 ULP。当除以 x 时,只有当 x == 0 时才会发生除以零的情况(溢出可能会发生,但已经指出)。 - Pascal Cuoq
没错,这不是原问题的答案,只是与epsilon相关的一条评论。我本应该将其作为评论放入其中,但之前的许多帖子都提到了这一点。但你的评论是正确的。更多信息请参见[http://en.cppreference.com/w/cpp/types/numeric_limits/epsilon]。 - mcjoan

-1

你可以尝试一下

if ((b-a)!=(a-b) && ((result = c/(b-a)) > 0) && (result < small_number))) {
...

不太确定,但是你确定在浮点数中(b-a)(a-b)会产生相同的0版本吗? - Bingo

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