TL; DR
- 使用以下函数代替当前接受的解决方案,以免在某些极限情况下产生某些不良结果,同时可能会提高效率。
- 了解数字的预期不精确度,并在比较函数中相应地输入它们。
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
return diff < std::max(relth, epsilon * norm);
}
图形,好吗?
比较浮点数时,有两个“模式”。
第一个是在相对模式,其中的区别x
和y
相对认为它们的振幅|x| + |y|
。以2D绘制时,它具有以下轮廓,其中绿色表示x
和相等y
。(epsilon
出于说明目的,我选择了0.5)。
相对模式是用于“正常”或“足够大”浮点值的模式。(稍后会详细介绍)。
第二种是绝对模式,当我们简单地将它们的差异与固定数字进行比较时。它给出以下轮廓(再次以epsilon
0.5和relth
1表示)。
这种绝对的比较模式用于“微小”的浮点值。
现在的问题是,我们如何将这两种响应模式缝合在一起。
在Michael Borgwardt的答案中,切换基于的值diff
,该值应低于relth
(Float.MIN_NORMAL
在他的答案中)。下图以阴影线显示了此切换区域。
因为该relth * epsilon
值较小relth
,所以绿色色块不会粘在一起,这反过来又给解决方案带来了不好的性质:我们可以找到这样的三元组,即x < y_1 < y_2
,而x == y2
但x != y1
。
请看以下引人注目的示例:
x = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32
我们拥有x < y1 < y2
,实际上y2 - x
比它大2000倍以上y1 - x
。但是使用当前的解决方案
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
相反,在以上提出的解决方案中,切换区域基于的值|x| + |y|
,该值由下面的阴影正方形表示。它可以确保两个区域正常连接。
另外,上面的代码没有分支,这可能会更有效率。考虑到操作,例如max
和abs
,其中先验需要支化,通常具有专用组装说明。出于这个原因,我认为这种方法优于另一种解决方案,该解决方案是nearlyEqual
通过将开关从更改为来修复Michael的diff < relth
方法diff < eps * relth
,这将产生基本相同的响应模式。
在相对和绝对比较之间切换?
这些模式之间的切换是围绕进行的relth
,这FLT_MIN
与接受的答案相同。这种选择意味着的表示形式float32
会限制我们的浮点数的精度。
这并不总是有意义。例如,如果您比较的数字是减法的结果,则可能在范围内FLT_EPSILON
更有意义。如果它们是相减数的平方根,则数字不精确度可能更高。
当您考虑将浮点数与进行比较时,这很明显0
。在这里,任何相对比较都会失败,因为|x - 0| / (|x| + 0) = 1
。因此,当x
您的计算不精确时,比较需要切换到绝对模式-很少会低到FLT_MIN
。
这就是引入relth
以上参数的原因。
而且,通过不relth
与乘epsilon
,该参数的解释很简单,并且与我们期望的这些数字的数值精度相对应。
数学隆隆声
(保留在这里主要是出于我的荣幸)
更一般而言,我假设行为良好的浮点比较运算符=~
应具有一些基本属性。
以下内容非常明显:
- 自我平等:
a =~ a
- 对称性:
a =~ b
暗示b =~ a
- 反对派的不变性:
a =~ b
暗示-a =~ -b
(我们没有a =~ b
并且b =~ c
暗示a =~ c
,=~
不是等价关系)。
我将添加以下特定于浮点比较的属性
- 如果
a < b < c
,则a =~ c
表示a =~ b
(更接近的值也应相等)
- 如果
a, b, m >= 0
则a =~ b
暗示a + m =~ b + m
(具有相同差异的较大值也应相等)
- 如果
0 <= λ < 1
然后a =~ b
暗示λa =~ λb
(可能不那么明显)。
这些属性已经对可能的近等函数给出了强大的约束。上面建议的功能可以验证它们。可能缺少一个或几个其他明显的属性。
当一个人认为是由和参数化=~
的一族平等关系时,也可以添加=~[Ɛ,t]
Ɛ
relth
- 如果
Ɛ1 < Ɛ2
然后a =~[Ɛ1,t] b
暗示a =~[Ɛ2,t] b
(给定公差的相等意味着更高公差的相等)
- 如果
t1 < t2
然后a =~[Ɛ,t1] b
暗示a =~[Ɛ,t2] b
(给定不精确度的相等意味着较高不精确度的相等)
提出的解决方案也对此进行了验证。