比较C++中的double,同行评审
Comparing double in C++, peer review
我一直有比较两个相等值的问题。周围有一些类似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
之前的double
与1
比较不相等,但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++语言要求不同类型的对象不别名。
有几种方法可以消除未定义的行为:
- 去掉并集,只将
memcpy
和double
转化为uint64_t
。便携式方式 - 用
[[gnu::may_alias]]
标记联合成员i
类型 - 在存储到联合成员
d
和从成员i
读取之间插入编译器内存屏障
这样框定问题:
- 我们有两个数字,
a
和b
,它们是用浮点运算计算的 - 如果它们是用实数数学精确计算的,我们就会有a和b
- 我们想比较
a
和b
,得到一个答案,告诉我们a是否等于b
换句话说,您正在尝试更正计算a
和b
时发生的错误。当然,总的来说,这是不可能的,因为我们不知道a和b是什么。我们只有CCD_ 29和CCD_。
你提出的代码可以追溯到另一种策略:
- 如果
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是否相等的实际方法是,在给定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、e<1sub>ar,e-<2sub>bl和e-br的边界。但是浮点格式并不能修复这些错误。它们不是2个ULP或1个ULP,也不是按指数缩放的任何数量的ULP。它们取决于CCD_ 60和CCD_。一般来说,误差的范围可以从0到无穷大,也可以是NaN。
因此,要测试a和b是否相等,需要分析可能发生的浮点算术错误。总的来说,这很困难。它有一个完整的数学领域,数值分析。
如果你已经计算出了误差的界限,那么你就可以使用普通的算术来比较区间。不需要拆开浮点表示并使用位。只需使用普通的加法、减法和比较运算。
(这个问题实际上比我上面允许的更复杂。给定一个计算值a
,a的势值并不总是位于一个区间内。它们可以是任意的点集。)
正如我之前所写的,没有一个通用的解决方案来比较包含算术错误的数字:0 1 2 3。
如果a和b可能相等,一旦你计算出错误边界并编写了一个返回true的测试,你仍然会遇到测试也接受假阴性的问题:即使a与b不相等,它也会返回true。换句话说,您刚刚替换了一个错误的程序,因为它拒绝相等,即使a和b在其他情况下是相等的,因为它在a与b不相等的情况下接受相等。这是没有通用解决方案的另一个原因:在一些应用程序中,接受不相等的数字是可以的,至少在某些情况下是这样。在其他应用程序中,这是不好的,使用这样的测试会破坏程序。
- C++在数学计算中将double转换为int
- 在c++中为double类型的数组创建一个unique_ptr
- vector<vector<double>> to mxArray using memcpy
- (double) 和 double() 之间的区别
- C++标准是否允许<double>在没有开销的情况下实现 std::可选
- C++ Version Of Double.longBitsToDouble
- 错误:类型"double()"和"double()"的操作数无效到二进制&quo
- 有没有办法在C++的赋值中将"char**"转换为"double"?
- 为什么 nlohmann/json 序列化 "null" 而不是在 double 上"0"?
- 使用 std::vector<double> 访问由 std::unique_ptr<double[2] 管理的数据>
- 将 std::vector<double> 从 C++ 包装到 C 以在 Swift 中使用
- 错误:无法将"<lambda(double)>"转换为"double(*)(double)"
- 如何正确将"strings"转换为"double"?
- 如何在<double>矢量中创建复杂类型的模板<T>?
- "double* grade"、"double *grade"和"double* fn()"有什么区别?
- 在不损失精度的情况下将double从C++传输到python
- 重复使用预分配的向量<复杂<double>>作为<double>长度两倍的向量
- 编译器给出错误:format 指定类型 'float *',但参数的类型'double' [-Wformat]
- Zedboard zynq-7000 Opencl 浮点数从类型 'double*' 强制转换为类型 'double' 无效
- 比较C++中的double,同行评审