如何进行浮点数比较?

126

我正在编写一些代码,其中有类似以下内容的部分:

double a = SomeCalculation1();
double b = SomeCalculation2();

if (a < b)
    DoSomething2();
else if (a > b)
    DoSomething3();

然后在其他地方,我可能需要进行相等性比较:

double a = SomeCalculation3();
double b = SomeCalculation4();

if (a == 0.0)
   DoSomethingUseful(1 / a);
if (b == 0.0)
   return 0; // or something else here

简而言之,我需要进行大量浮点数计算,并且需要进行各种条件比较。在此情况下,将其转换为整数运算是没有意义的。

我之前读到过关于浮点数比较不可靠的文章,因为可能出现这样的情况:

double a = 1.0 / 3.0;
double b = a + a + a;
if ((3 * a) != b)
    Console.WriteLine("Oh no!");

简言之,我想知道:如何可靠地比较浮点数(小于、大于、相等)?

我使用的数字范围大约从10E-14到10E6,因此我需要处理小数和大数。

我将此标记为语言无关,因为我想知道我可以在任何语言中实现这个功能。


使用浮点数时,无法可靠地完成此操作。计算机始终会存在一些数字,它们在现实中并不相等,但对于计算机来说却是相等的(例如1E + 100,1E + 100 + 1),而通常还会有一些计算结果,在现实中它们是相等的,但对于计算机来说却不相等(请参见nelhage回答中的评论之一)。您必须选择其中一个优先级较低的结果。 - toochin
另一方面,如果您只涉及有理数,可以基于整数实现一些有理数算术运算,然后当其中一个数字被约分为另一个数字时,这两个数字被视为相等。 - toochin
目前我正在进行一项模拟工作。我通常进行这些比较的地方与可变时间步长有关(用于解决某些ode)。有几种情况需要检查一个对象的给定时间步长是否等于、小于或大于另一个对象的时间步长。 - Mike Bailey
浮点数比较的最有效方法 - phuclv
13个回答

108

简述

  • 使用下面的函数代替当前被接受的解决方案,以避免在某些极限情况下出现一些不良结果,同时可能更加有效。
  • 知道您的数字上可能存在的预期不精确性,并相应地将其输入到比较函数中进行比较。
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);
}

请提供图形?

在比较浮点数时,有两种“模式”。

第一种是相对模式,其中将xy之间的差异相对于它们的振幅|x| + |y|考虑。在2D绘图中,它给出以下轮廓,绿色表示xy的相等性。(我为说明目的取了一个0.5的epsilon。)

enter image description here

相对模式用于“正常”或“足够大”的浮点值(稍后再详细解释)。第二种是“绝对”模式,当我们仅将它们的差异与一个固定数字进行比较时。它给出了以下轮廓(再次使用epsilon为0.5和abs_th为1进行说明)。

enter image description here

这种绝对比较模式用于“微小”的浮点值。
现在的问题是,我们如何将这两个响应模式组合起来。
在Michael Borgwardt的答案中,开关基于diff的值,应该低于abs_th(在他的答案中为Float.MIN_NORMAL)。在下面的图表中,此开关区域显示为阴影部分。

enter image description here

由于 abs_th * epsilonabs_th 更小,绿色区域不会粘在一起,这反过来导致解决方案具有一个不好的属性:我们可以找到三个数字的三元组,使得 x < y_1 < y_2 但是 x == y2x != y1

enter image description here

以这个惊人的例子为例:

x  = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32

我们有 x < y1 < y2,实际上 y2 - xy1 - x 大2000多倍。然而,当前的解决方案却不能解决这个问题。
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True

相比之下,上述提出的解决方案中,开关区域是基于|x| + |y|的值来确定的,如下方阴影正方形所示。这确保了两个区域之间的连接更加顺畅。

enter image description here

此外,上面的代码没有分支,这可能更有效。考虑到像maxabs之类的操作,需要先验地进行分支,通常具有专用的汇编指令。因此,我认为这种方法优于另一种解决方案,即通过将开关从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 =~ bb =~ 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`(给定不精确度下的相等性意味着更高不精确度下的相等性)。这个解决方案也验证了这些。

4
C++ 实现问题:(std::abs(a) + std::abs(b)) 是否可能大于 std::numeric_limits<float>::max() - anneb
5
可以,它可以是正无穷。 - Paul Groke
你的代码中参数名称似乎颠倒了。'relth'参数被用作绝对阈值,而'epsilon'参数被用作相对阈值。 - andypea
1
@andypea 谢谢。其实只是名称很糟糕 - 我已经改为更有意义的 abs_th 了。 - P-Gn
虽然代码可以翻译,但是你不能将其与所选解决方案进行比较,因为它们是两种不同的语言,因此你不能说“选择这个而不是那个”。 - L4ZZA
显示剩余3条评论

83

只要你的工作不接近float/double精度限制的边缘,比较大小并不是真正的问题。

对于“模糊相等”的比较,我在《浮点运算指南》中经过大量努力和考虑到许多批评后得出了以下Java代码(应很容易适应):

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

它自带测试套件。如果没有测试套件,那么你应该立即排除任何解决方案,因为它几乎肯定会在某些边缘情况下失败,例如只有一个值为0、两个非常接近于零的相反值或无限大。

另一种选择(请参见上面的链接了解更多细节)是将浮点数的位模式转换为整数,并接受一个固定整数距离内的所有内容。

无论如何,可能没有任何一种解决方案适用于所有应用程序。理想情况下,您应该开发/调整自己的测试套件,以覆盖您实际使用的情况。


1
@toochin:取决于您想允许多大的误差范围,但当考虑到最接近零的非规格化数字(正数和负数)时,它就变成了一个明显的问题- 除了零以外,它们比任何其他两个值都更接近,然而许多基于相对误差的天真实现将认为它们距离太远。 - Michael Borgwardt
2
е—ҜгҖӮдҪ жңүдёҖдёӘжөӢиҜ•else if (a * b == 0)пјҢдҪҶжҳҜеҗҢдёҖиЎҢдёҠзҡ„жіЁйҮҠжҳҜaжҲ–bжҲ–дёӨиҖ…йғҪжҳҜйӣ¶гҖӮдҪҶиҝҷдёҚжҳҜдёӨ件дёҚеҗҢзҡ„дәӢеҗ—пјҹдҫӢеҰӮпјҢеҰӮжһңa == 1e-162дё”b == 2e-162пјҢеҲҷжқЎд»¶a * b == 0е°ҶдёәзңҹгҖӮ - Mark Dickinson
1
@toochin:主要是因为代码应该易于移植到其他可能没有该功能的语言中(它也仅在Java 1.5中添加)。 - Michael Borgwardt
1
非常好的指南和答案,特别是考虑到这里的abs(a-b)<eps答案。两个问题:(1)将所有更改为<= 是否更好,从而允许“零eps”比较,相当于精确比较? (2)使用diff <epsilon *(absA + absB);而不是diff /(absA + absB)<epsilon;(最后一行)是否更好 -? - Franz D.
1
这不是一个好的回答。实际上,你几乎总是需要使用数字之间的绝对差,例如 nearlyEqual(a, b, eps) 给出与所有可能的 ab 相同的答案,就像 nearlyEqual(a-b, 0, eps) 一样。你需要知道你所处理的数字的公差,然后使用一个简单的绝对方法,比如 https://dev59.com/_W445IYBdhLWcg3wUoxe#19050413。 - fishinear
显示剩余11条评论

16

我有一个比较浮点数的问题,即比较A < BA > B。下面的方法看起来有效:

if(A - B < Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is less than B");
}

if (A - B > Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is greater than B");
}

fabs函数(求绝对值)会确保两个数在本质上是否相等。


4
完全不需要使用 fabs,只要在第一个测试中写成 if (A - B < -Epsilon) 即可。 - fishinear

12

我们必须选择一个公差级别来比较浮点数。例如,

final float TOLERANCE = 0.00001;
if (Math.abs(f1 - f2) < TOLERANCE)
    Console.WriteLine("Oh yes!");

注:你的例子相当有趣。

double a = 1.0 / 3.0;
double b = a + a + a;
if (a != b)
    Console.WriteLine("Oh no!");

这里有一些数学内容

a = 1/3
b = 1/3 + 1/3 + 1/3 = 1.

1/3 != 1

哦,是的..

你是指

if (b != 1)
    Console.WriteLine("Oh no!")

4

我对Swift中浮点数比较的想法

infix operator ~= {}

func ~= (a: Float, b: Float) -> Bool {
    return fabsf(a - b) < Float(FLT_EPSILON)
}

func ~= (a: CGFloat, b: CGFloat) -> Bool {
    return fabs(a - b) < CGFloat(FLT_EPSILON)
}

func ~= (a: Double, b: Double) -> Bool {
    return fabs(a - b) < Double(FLT_EPSILON)
}

1
你应该问自己为什么要比较这些数字。如果你知道比较的目的,那么你也应该知道数字所需的精度。每种情况和每个应用环境都不同。但在几乎所有实际情况下都需要绝对精度。只有极少数情况适用于相对精度。
举个例子:如果你的目标是在屏幕上绘制图形,那么你可能希望浮点值在映射到屏幕上的同一像素时相等。如果你的屏幕大小为1000像素,而你的数字在1e6范围内,那么你可能希望100等于200。
给定所需的绝对精度,那么算法变为:
public static ComparisonResult compare(float a, float b, float accuracy) 
{
    if (isnan(a) || isnan(b))   // if NaN needs to be supported
        return UNORDERED;    
    if (a == b)                 // short-cut and takes care of infinities
        return EQUAL;           
    if (abs(a-b) < accuracy)    // comparison wrt. the accuracy
        return EQUAL;
    if (a < b)                  // larger / smaller
        return SMALLER;
    else
        return LARGER;
}

1

从Michael Borgwardt和bosonix的答案中对PHP进行适应:

class Comparison
{
    const MIN_NORMAL = 1.17549435E-38;  //from Java Specs

    // from http://floating-point-gui.de/errors/comparison/
    public function nearlyEqual($a, $b, $epsilon = 0.000001)
    {
        $absA = abs($a);
        $absB = abs($b);
        $diff = abs($a - $b);

        if ($a == $b) {
            return true;
        } else {
            if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) {
                return $diff < ($epsilon * self::MIN_NORMAL);
            } else {
                return $diff / ($absA + $absB) < $epsilon;
            }
        }
    }
}

0
我想出了一种简单的方法来调整 epsilon 的大小以适应被比较数字的大小。因此,不再使用以下方式:
iif(abs(a - b) < 1e-6, "equal", "not")

如果ab可能很大,我将其更改为:

iif(abs(a - b) < (10 ^ -abs(7 - log(a))), "equal", "not")

我想这并不能满足其他答案中讨论的所有理论问题,但它有一个优点,那就是只需要一行代码,因此可以在Excel公式或Access查询中使用,而不需要VBA函数。

我搜索了一下,看看是否有其他人使用过这种方法,但我没有找到任何信息。我在我的应用程序中进行了测试,似乎运行良好。因此,它似乎是一种适用于不需要其他答案复杂性的情境的方法。但我想知道是否存在我没有考虑到的问题,因为似乎没有其他人在使用它。

如果有原因导致对各种大小的数字进行简单比较时,使用对数测试无效,请在评论中说明原因。


0

标准建议是使用一些小的“epsilon”值(根据您的应用程序选择),并将在epsilon范围内的浮点数视为相等。例如:

#define EPSILON 0.00000001

if ((a - b) < EPSILON && (b - a) < EPSILON) {
  printf("a and b are about equal\n");
}

更完整的答案比较复杂,因为浮点数误差非常微妙和混淆。如果你真的关心任何精确意义上的相等性,你可能正在寻求一种不涉及浮点数的解决方案。

如果他正在使用非常小的浮点数,比如2.3E-15,会怎么样? - toochin
1
我正在处理大约[10E-14, 10E6]的范围,不完全等同于机器精度但非常接近它。 - Mike Bailey
2
如果您记住必须处理相对误差,那么使用小数并不是问题。如果您不关心相对较大的误差容限,那么将上述条件替换为类似于if ((a - b) < EPSILON/a && (b - a) < EPSILON/a)的内容就可以了。 - toochin
2
上面给出的代码在处理非常大的数字c时也存在问题,因为一旦您的数字足够大,EPSILON将小于c的机器精度。例如,假设c = 1E + 22; d = c / 3; e = d + d + d;。那么e-c可能远大于1。 - toochin
1
例如,尝试 double a = pow(8,20); double b = a/7; double c = b+b+b+b+b+b+b; std::cout<<std::scientific<<a-c;(根据pnt和nelhage,a和c不相等),或者 double a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;(根据pnt和nelhage,a和b相等)。 - toochin
更多信息:尽管数字范围相当大,但我的代码几乎总是处理接近一个数量级的数字。我不会遇到像a = 1.0,b = 1000000这样的情况。它更像是a = 1.001,b = 2.002。 - Mike Bailey

0

我试着根据上述评论编写一个相等函数。这是我想出来的:

编辑:从 Math.Max(a, b) 更改为 Math.Max(Math.Abs(a), Math.Abs(b))

static bool fpEqual(double a, double b)
{
    double diff = Math.Abs(a - b);
    double epsilon = Math.Max(Math.Abs(a), Math.Abs(b)) * Double.Epsilon;
    return (diff < epsilon);
}

你有什么想法?我还需要解决大于和小于的问题。


epsilon 应该是 Math.abs(Math.Max(a, b)) * Double.Epsilon;,否则对于负数的 ab,它总是小于 diff。而且我认为你的 epsilon 太小了,函数可能不会返回与 == 运算符不同的任何内容。大于是 a < b && !fpEqual(a,b) - toochin
1
当两个值完全为零时失败,对于Double.Epsilon和-Double.Epsilon也会失败,对于无限大也会失败。 - Michael Borgwardt
1
我的特定应用程序中无穷大的情况并不是一个问题,但已经注意到了。 - Mike Bailey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接