TL;DR
- 使用以下函数代替当前接受的解决方案,以避免在某些限制情况下出现一些不良结果,同时可能更有效。
- 了解您对数字的预期不精确度,并在比较函数中相应地提供它们。
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float abs_th = FLT_MIN)
// those defaults are arbitrary and could be removed
{
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());
// or even faster: std::min(std::abs(a + b), std::numeric_limits<float>::max());
// keeping this commented out until I update figures below
return diff < std::max(abs_th, epsilon * norm);
}
请提供图形?
比较浮点数时,有两种“模式”。
第一个是相对模式,其中x和y之间的差异被认为是相对于它们的幅度|x| + |y|。在 2D 中绘制时,它给出以下配置文件,其中绿色表示 x 和 y 相等。 (出于说明目的,我将epsilon 设为 0.5)。
相对模式用于“正常”或“足够大”的浮点值。 (稍后会详细介绍)。
第二种是绝对模式,当我们简单地将它们的差异与一个固定数字进行比较时。它给出了以下配置文件(再次使用 0.5 的 epsilon 和 1 的 abs_th 进行说明)。
这种绝对比较模式用于“微小”浮点值。
现在的问题是,我们如何将这两种响应模式拼接在一起。
在 Michael Borgwardt 的回答中,开关基于diff 的值,该值应低于abs_th(他的回答中为Float.MIN_NORMAL)。该开关区域如下图阴影所示。
因为abs_th * epsilon 比abs_th 小,所以绿色补丁不会粘在一起,这反过来又给解决方案带来了不好的属性:我们可以找到三组数字,例如x < y_1 < y_2 和x == y2 但@ 987654347@.
以这个惊人的例子为例:
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(可能不那么明显可以论证)。
这些属性已经对可能的近似相等函数提供了强大的约束。上面提出的功能验证了它们。也许缺少一个或几个明显的属性。
当人们将=~ 视为一组由Ɛ 和abs_th 参数化的等式关系=~[Ɛ,t] 时,还可以添加
- 如果
Ɛ1 < Ɛ2 则a =~[Ɛ1,t] b 暗示a =~[Ɛ2,t] b(给定容差的平等意味着更高容差的平等)
- 如果
t1 < t2 则a =~[Ɛ,t1] b 暗示a =~[Ɛ,t2] b(给定不精确度的相等意味着更高不精确度的相等)
建议的解决方案也验证了这些。