比较 C++ 中的双精度,同行评审
Posted
技术标签:
【中文标题】比较 C++ 中的双精度,同行评审【英文标题】:Comparing double in C++, peer review 【发布时间】:2019-11-18 09:52:20 【问题描述】:我一直有比较 double 值是否相等的问题。有一些类似fuzzy_compare(double a, double b)
的功能,但我经常没能及时找到它们。所以我想为比较运算符构建一个双精度包装类:
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,应该相等。接下来,比较指数。如果这些不同,则数字不同。最后,比较尾数。如果差异足够小,则值相等。
它似乎有效,但可以肯定的是,我想要同行评审。很可能是我忽略了一些东西。
是的,这个包装类需要所有运算符重载的东西。我跳过了,因为它们都是微不足道的。相等运算符是这个包装类的主要用途。
【问题讨论】:
评论不用于扩展讨论;这个对话是moved to chat。 【参考方案1】:这段代码有几个问题:
零不同边的小值总是比较不相等,无论相距多远(不)。
更重要的是,-0.0
与+epsilon
比较不相等,但+0.0
与+epsilon
比较相等(对于某些epsilon
)。这真的很糟糕。
NaN 呢?
具有不同指数的值比较不相等,即使相隔一个浮点“步长”(例如,1
之前的 double
与 1
比较不相等,但 1
之后的比较相等...)。
讽刺的是,最后一点可以通过不区分指数和尾数来解决:所有正浮点数的二进制表示完全按照它们的数量级!
您似乎只想检查两个浮点数是否相隔一定数量的“步数”。如果是这样,也许this boost function 可能会有所帮助。但我也会质疑这是否真的合理:
最小的正非非正规比较应该等于零吗?它们之间仍然有许多(非正规)浮点数。我怀疑这是你想要的。
如果您对预期大小为 1e16 的值进行运算,则 1
的比较结果应等于 0
,即使所有正数 double
s 中有一半介于 0 和 1 之间。
使用相对 + 绝对 epsilon 通常是最实用的。但我认为看看这篇文章是最值得的,它更广泛地讨论了比较浮点数的主题,而不是我能适应这个答案:
https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
引用其结论:
知道你在做什么
没有灵丹妙药。你必须明智地选择。
如果您要与零进行比较,那么基于相对 epsilon 和 ULP 的比较通常是没有意义的。您需要使用绝对 epsilon,其值可能是FLT_EPSILON
的一些小倍数以及计算的输入。也许吧。 如果您要与非零数进行比较,那么基于相对 epsilon 或 ULP 的比较可能是您想要的。您可能需要一些FLT_EPSILON
的小倍数作为您的相对 epsilon,或者一些少量的 ULP。如果您确切知道要比较的数字,则可以使用绝对 epsilon。 如果您要比较两个可能为零或非零的任意数字,那么您需要厨房水槽。祝你好运,祝你好运。最重要的是,您需要了解您正在计算什么、算法的稳定性以及如果误差大于预期应该怎么办。浮点数学可以非常准确,但您还需要了解您实际计算的是什么。
【讨论】:
感谢您的回答。是的,怪胎已经提到了第一点。我也必须在这里添加一些模糊性。第二点确实不好。我会寻找这个的解决方案。 NaN 应该在这里比较正确(所有尾数和指数位都设置为 1)。最后一点很有趣。是的,也许我应该把它们放在一起比较。 @Siegfried 有许多不同的 NaN 值,它们当然不会将所有非符号位都设置为 1。每个值都应该根据不等于任何 NaN 进行比较(即使按位相同)到 IEEE 754,我认为这是不明智的。【参考方案2】:您存储到一个联合成员中,然后从另一个成员中读取。这会导致别名问题(未定义的行为),因为 C++ 语言要求不同类型的对象不要别名。
有几种方法可以删除未定义的行为:
-
摆脱联合,只需将
memcpy
将double
转换为uint64_t
。便携的方式。
用[[gnu::may_alias]]
标记联合成员i
类型。
在存储到联合成员 d
和从成员 i
读取之间插入编译器内存屏障。
【讨论】:
嗯,真的有“混叠”问题吗?工会只有 2 名成员,大小完全相同。如果工会有一个结构作为成员,我可以想象这样的问题。但是一个简单的原始类型?编译器应该如何以及为什么将一个原始类型分布在不相邻的字节上,并且与另一个相同大小的原始类型不同? @Siegfried 好吧,union-cast 是一种广泛使用的现有做法,因此不太可能破坏,尽管 C++ 标准声明加载非活动联合成员是未定义的行为。但是,将其替换为memcpy
会在此处完全以 0 成本消除未定义的行为以及特殊处理。人们确实报告说这种联合转换在其他不涉及堆栈上的联合的情况下会中断。【参考方案3】:
以这种方式提出问题:
我们有两个数字,a
和 b
,它们是使用浮点算法计算出来的。
如果它们是用实数数学精确计算出来的,我们就会有 a 和 b。
我们想比较 a
和 b
并得到一个答案,告诉我们 a 是否等于 b。
换句话说,您正在尝试纠正在计算a
和b
时发生的错误。一般来说,这是不可能的,当然,因为我们不知道 a 和 b 是什么。我们只有a
和b
的近似值。
您提出的代码回退到另一个策略:
如果a
和b
彼此接近,我们将接受a 等于b。 (换句话说:如果a
接近b
,a等于b是可能只是因为计算错误,所以我们将接受 a 等于 b 没有进一步的证据。)
这个策略有两个问题:
即使a
和b
接近,此策略也会错误地接受a 等于b,即使它不正确。
我们需要确定要求 a
和 b
的距离。
您的代码试图解决后者:它正在建立一些关于a
和b
是否足够接近的测试。正如其他人指出的那样,它存在严重缺陷:
a
为负数,反之亦然。
如果数字具有不同的指数,它会将数字视为不同,但浮点运算会导致 a
具有与 a 不同的指数。
如果数字的差异超过固定数量的 ULP(最低精度单位),它会将数字视为不同,但浮点运算通常会导致 a
与 a 不限金额。
它采用 IEEE-754 格式,并不必要地使用了 C++ 标准未定义的行为的别名。
该方法存在根本缺陷,因为它不必要地摆弄了浮点表示。从a
和b
确定 a 和 b might 是否相等的实际方法是找出a
和b
,a 和 b 有哪些值集合以及这些集合中是否有任何共同值。
换句话说,给定a
,a 的值可能在某个区间内,(a
-eal , a
+ear) (即从a
减去左边的一些错误到a
加上右边的一些错误) ,并且,给定b
,b 的值可能在某个区间内,(b
−ebl, b
+ebr)。如果是这样,您要测试的不是一些浮点表示属性,而是两个区间(a
−eal、a
+ ear) 和 (b
−ebl, b
+ebr) 重叠。
要做到这一点,您需要了解或至少了解错误 eal、ear 、ebl 和 ebr。但是浮点格式并不能修复这些错误。它们不是 2 个 ULP 或 1 个 ULP 或任何按指数缩放的 ULP。它们取决于a
和b
的计算方式。一般来说,误差范围可以从0到无穷大,也可以是NaN。
因此,要测试 a 和 b 是否相等,您需要分析可能发生的浮点算术错误。一般来说,这很困难。它有一个完整的数学领域,numerical analysis。
如果您已经计算出错误的界限,那么您可以使用普通算术来比较区间。无需拆开浮点表示并使用位。只需使用正常的加法、减法和比较操作即可。
(问题实际上比我上面允许的要复杂。给定一个计算值a
,a 的潜在值并不总是位于单个区间内。它们可以是任意集合点数。)
正如我之前写的,没有比较包含算术错误的数字的通用解决方案:0123。
一旦你找出错误界限并编写一个如果 a 和 b 可能相等则返回 true 的测试,你仍然会遇到测试也接受假阴性的问题: 即使在 a 和 b 不相等的情况下它也会返回 true。换句话说,您刚刚替换了一个错误的程序,因为它拒绝相等,即使 a 和 b 与在其他情况下错误的程序相等,因为它在 a 和 b 不相等的情况下接受相等。这是没有通用解决方案的另一个原因:在某些应用程序中,接受不相等的相等数字是可以的,至少在某些情况下是这样。在其他应用程序中,这是不行的,使用这样的测试会破坏程序。
【讨论】:
好吧,对我来说没关系。我不计算这些数字。在大多数情况下,它们是由用户以某种方式给出的(在输入字段中编辑)。我所要做的就是检查值是否已更改。问题是,对于具有不同参数的不同方法,幅度可能会发生巨大变化。而且我事先不知道幅度。所以 f.ex.在一种方法中,我必须检查 123456789.1234567 和 123456789.1234568 的旧值,在另一种方法中,我必须检查 0.00023 和值 0.00021 的旧值。我需要一些能抓住一切的东西。以上是关于比较 C++ 中的双精度,同行评审的主要内容,如果未能解决你的问题,请参考以下文章