简述
- 使用下面的函数代替当前被接受的解决方案,以避免在某些极限情况下出现一些不良结果,同时可能更加有效。
- 知道您的数字上可能存在的预期不精确性,并相应地将其输入到比较函数中进行比较。
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float abs_th = 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(abs_th, epsilon * norm);
}
请提供图形?
在比较浮点数时,有两种“模式”。
第一种是相对模式,其中将x
和y
之间的差异相对于它们的振幅|x| + |y|
考虑。在2D绘图中,它给出以下轮廓,绿色表示x
和y
的相等性。(我为说明目的取了一个0.5的epsilon
。)
相对模式用于“正常”或“足够大”的浮点值(稍后再详细解释)。第二种是“绝对”模式,当我们仅将它们的差异与一个固定数字进行比较时。它给出了以下轮廓(再次使用epsilon为0.5和abs_th为1进行说明)。
这种绝对比较模式用于“微小”的浮点值。
现在的问题是,我们如何将这两个响应模式组合起来。
在Michael Borgwardt的答案中,开关基于diff的值,应该低于abs_th(在他的答案中为Float.MIN_NORMAL)。在下面的图表中,此开关区域显示为阴影部分。
由于
abs_th * epsilon
比
abs_th
更小,绿色区域不会粘在一起,这反过来导致解决方案具有一个不好的属性:我们可以找到三个数字的三元组,使得
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
比
y1 - x
大2000多倍。然而,当前的解决方案却不能解决这个问题。
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
相比之下,上述提出的解决方案中,开关区域是基于
|x| + |y|
的值来确定的,如下方阴影正方形所示。这确保了两个区域之间的连接更加顺畅。
此外,上面的代码没有分支,这可能更有效。考虑到像
max
和
abs
之类的操作,需要先验地进行分支,通常具有专用的汇编指令。因此,我认为这种方法优于另一种解决方案,即通过将开关从
diff < abs_th
改为
diff < eps * abs_th
来修复Michael的
nearlyEqual
,这将产生基本相同的响应模式。
在何处在相对比较和绝对比较之间切换?
这些模式之间的切换是在
abs_th
周围进行的,在接受的答案中取为
FLT_MIN
。这个选择意味着
float32
的表示限制了我们浮点数的精度。
这并不总是有意义的。例如,如果要比较的数字是减法的结果,则范围在
FLT_EPSILON
左右可能更合适。如果它们是减去数字的平方根,则数值不精确性可能会更高。
当你比较浮点数与
0
时,如果考虑到任何相对比较都会失败,因为
|x-0|/(|x|+0)=1
,所以在
x
的数量级达到计算的不确定性时,比较需要转换为绝对模式,而很少低于
FLT_MIN
。这就是上面引入
abs_th
参数的原因。
此外,通过不将
abs_th
乘以
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]`组成的等式关系族时,可以添加以下内容:如果`Ɛ1 < Ɛ2`,则`a =~[Ɛ1,t] b`意味着`a =~[Ɛ2,t] b`(给定容差下的相等性意味着更高容差下的相等性);如果`t1 < t2`,则`a =~[Ɛ,t1] b`意味着`a =~[Ɛ,t2] b`(给定不精确度下的相等性意味着更高不精确度下的相等性)。这个解决方案也验证了这些。