比较C++中的double,同行评审

Comparing double in C++, peer review

本文关键字:评审 double C++ 中的 比较      更新时间:2023-10-16

我一直有比较两个相等值的问题。周围有一些类似fuzzy_compare(double a, double b)的函数,但我经常没能及时找到它们。所以我想为double构建一个包装类,只为比较运算符:

typedef union {
uint64_t i;
double d;
} number64;
bool Double::operator==(const double value) const {
number64 a, b;
a.d = this->value;
b.d = value;
if ((a.i & 0x8000000000000000) != (b.i & 0x8000000000000000)) {
if ((a.i & 0x7FFFFFFFFFFFFFFF) == 0 && (b.i & 0x7FFFFFFFFFFFFFFF) == 0)
return true;
return false;
}
if ((a.i & 0x7FF0000000000000) != (b.i & 0x7FF0000000000000))
return false;
uint64_t diff = (a.i & 0x000FFFFFFFFFFFF) - (b.i & 0x000FFFFFFFFFFFF) & 0x000FFFFFFFFFFFF;
return diff < 2;    // 2 here is kind of some epsilon, but integer and independent of value range
}

其背后的理念是:首先,比较符号。如果符号不同,数字也不同。除非所有其他位都为零。也就是说,将+0.0与-0.0进行比较,这应该是相等的。接下来,比较指数。如果这些不同,数字就不同。最后,比较尾数。如果差值足够低,则值相等。

这似乎有效,但可以肯定的是,我想进行同行评审。很可能是我忽略了什么。

是的,这个包装类需要所有运算符重载的东西。我跳过了,因为它们都很琐碎。相等运算符是这个包装类的主要用途。

此代码有几个问题:

  • 零的不同边上的小值总是比较不相等,无论相距多远。

  • 更重要的是,-0.0+epsilon相比不相等,但+0.0+epsilon相比相等(对于某些epsilon)。这真的很糟糕。

  • NaN呢?

  • 具有不同指数的值比较不相等;步骤";分开(例如,1之前的double1比较不相等,但1之后的比较相等…)

具有讽刺意味的是,最后一点可以通过不区分指数和尾数来固定:所有正浮点的二进制表示正是它们的数量级!

看起来你只想检查两个浮点是否是某个数目的"浮点";步骤";分开地如果是这样的话,也许这个助推功能会有所帮助。但我也会质疑这是否真的合理:

  • 最小的正非标准化比较是否应该等于零?它们之间仍然存在许多(非规范化)浮动。我怀疑这是你想要的。

  • 如果对预期大小为1e16的值进行运算,则1应与0相比较,即使所有正double的一半在0和1之间。

使用相对+绝对ε通常是最实用的。但我认为最值得一看的是这篇文章,它更广泛地讨论了比较浮动的主题,我无法将其纳入这个答案:

https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/

引用其结论:

知道自己在做什么

没有银弹。你必须做出明智的选择。

  • 如果你与零进行比较,那么基于相对ε和ULP的比较通常是没有意义的。您需要使用绝对ε,其值可能是FLT_EPSILON和计算输入的一些小倍数。也许吧
  • 如果你正在与一个非零值进行比较,那么基于相对ε或ULP的比较可能就是你想要的。你可能想要一些FLT_EPSILON的小倍数作为你的相对ε,或者一些小数量的ULP。如果你确切地知道你要与哪个数字进行比较,那么可以使用绝对ε
  • 如果你正在比较两个可能为零或非零的任意数字,那么你需要厨房水槽。祝你好运,速度快

最重要的是,你需要了解你在计算什么,算法的稳定性如何,以及如果误差大于预期,你应该怎么做。浮点数学可能非常精确,但你也需要了解你实际计算的是什么。

存储到一个联合成员中,然后从另一个成员中读取。这导致了别名问题(未定义的行为),因为C++语言要求不同类型的对象不别名。

有几种方法可以消除未定义的行为:

  1. 去掉并集,只将memcpydouble转化为uint64_t。便携式方式
  2. [[gnu::may_alias]]标记联合成员i类型
  3. 在存储到联合成员d和从成员i读取之间插入编译器内存屏障

这样框定问题:

  • 我们有两个数字,ab,它们是用浮点运算计算的
  • 如果它们是用实数数学精确计算的,我们就会有ab
  • 我们想比较ab,得到一个答案,告诉我们a是否等于b

换句话说,您正在尝试更正计算ab时发生的错误。当然,总的来说,这是不可能的,因为我们不知道ab是什么。我们只有CCD_ 29和CCD_。

你提出的代码可以追溯到另一种策略:

  • 如果ab彼此接近,我们将接受a等于b。(换句话说:如果a接近b,则可能a等于b,并且我们的差异只是因为计算错误,因此我们将在没有进一步证据的情况下接受a=b。)

此策略存在两个问题:

  • 此策略将错误地接受a等于b,即使这不是真的,因为ab很接近
  • 我们需要决定ab的接近程度

您的代码试图解决后者:它正在建立一些关于ab是否足够接近的测试。正如其他人所指出的,它存在严重缺陷:

  • 如果数字有不同的符号,它会将其视为不同的数字,但即使a为正,浮点运算也会导致a为负,反之亦然
  • 如果数字的指数不同,它会将其视为不同的数字,但浮点运算可能会导致a的指数与的指数不同
  • 如果数字的差异超过固定数量的ULP(最低精度单位),它会将其视为不同的数字,但浮点运算通常会导致aa相差任何数量
  • 它采用IEEE-754格式,并且不必要地使用C++标准未定义的行为进行混叠

该方法存在根本缺陷,因为它不必要地篡改了浮点表示。根据ab确定ab是否相等的实际方法是,在给定ab的情况下,找出ab具有哪些值集,以及这些值集中是否有任何共同值。

换言之,给定aa的值可能在某个区间内,(aeala+ear)(即,从aa的所有数字减去左侧的一些误差加右侧的一些误差),并且,给定bb值可能在某种区间内,(beblb+ebr)。如果是,则要测试的不是某些浮点表示属性,而是两个间隔(aeala+ear)和(beblb+ebr

要做到这一点,您需要知道或至少有关于错误eale<1sub>ar,e-<2sub>bl和e-br的边界。但是浮点格式并不能修复这些错误。它们不是2个ULP或1个ULP,也不是按指数缩放的任何数量的ULP。它们取决于CCD_ 60和CCD_。一般来说,误差的范围可以从0到无穷大,也可以是NaN。

因此,要测试ab是否相等,需要分析可能发生的浮点算术错误。总的来说,这很困难。它有一个完整的数学领域,数值分析。

如果你已经计算出了误差的界限,那么你就可以使用普通的算术来比较区间。不需要拆开浮点表示并使用位。只需使用普通的加法、减法和比较运算。

(这个问题实际上比我上面允许的更复杂。给定一个计算值aa的势值并不总是位于一个区间内。它们可以是任意的点集。)

正如我之前所写的,没有一个通用的解决方案来比较包含算术错误的数字:0 1 2 3。

如果ab可能相等,一旦你计算出错误边界并编写了一个返回true的测试,你仍然会遇到测试也接受假阴性的问题:即使ab不相等,它也会返回true。换句话说,您刚刚替换了一个错误的程序,因为它拒绝相等,即使ab在其他情况下是相等的,因为它在ab不相等的情况下接受相等。这是没有通用解决方案的另一个原因:在一些应用程序中,接受不相等的数字是可以的,至少在某些情况下是这样。在其他应用程序中,这是不好的,使用这样的测试会破坏程序。