我该如何进行浮点比较?

时间:2011-02-06 19:12:11

标签: language-agnostic comparison floating-point

我正在编写一些代码,其中包含以下内容:

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,所以我确实需要使用小数字和大数字。

我已将此标记为语言无关,因为无论我使用何种语言,我都对如何实现此目标感兴趣。

12 个答案:

答案 0 :(得分:62)

比较大/小并不是一个问题,除非你在浮点/双精度限制的边缘工作。

对于“模糊等于”比较,这个(Java代码,应该很容易适应)是我在做了大量工作并考虑到许多批评之后为The Floating-Point Guide提出的:

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 :(得分:13)

我遇到了比较浮点数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");
}

晶圆厂 - 绝对价值 - 如果它们基本相同则负责。

答案 2 :(得分:11)

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

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!")

答案 3 :(得分:10)

TL; DR

  • 使用以下功能代替当前接受的解决方案,以避免在某些限制情况下出现一些不良后果,同时可能更有效。
  • 了解您对数字的预期不精确度,并在比较函数中相应地提供它们。
bool nearly_equal(
  float a, float b,
  float epsilon = 128 * FLT_EPSILON, float relth = 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());
  return diff < std::max(relth, epsilon * norm);
}

图形,好吗?

比较浮点数时,有两种&#34;模式&#34;。

第一个是相对模式,其中xy之间的差异被认为与其幅度|x| + |y|相对。在2D中绘制时,它给出以下轮廓,其中绿色表示xy的相等。 (为了便于说明,我拿了一个0.5的epsilon

enter image description here

相对模式是用于&#34;正常&#34;或者&#34;足够大&#34;浮点值。 (稍后会详细介绍)。

第二个是绝对模式,当我们简单地将它们的差异与固定数字进行比较时。它提供了以下配置文件(同样,epsilon为0.5,relth为1,用于说明。

enter image description here

这种绝对的比较模式是用于&#34;微小的&#34;浮点值。

现在问题是,我们如何将这两种反应模式拼接在一起。

在Michael Borgwardt的回答中,切换基于diff的值,该值应低于relth(答案中为Float.MIN_NORMAL)。此开关区域在下图中以阴影线显示。

enter image description here

因为relth * epsilon小于relth,绿色补丁不会粘在一起,这反过来又给解决方案带来了不好的属性:我们可以找到x < y_1 < y_2x == y2的数字三元组但是x != y1x = 4.9303807e-32 y1 = 4.930381e-32 y2 = 4.9309825e-32

enter image description here

举一个惊人的例子:

x < y1 < y2

我们有y2 - x,实际上y1 - xnearlyEqual(x, y1, 1e-4) == False nearlyEqual(x, y2, 1e-4) == True 大2000多倍。然而,使用当前的解决方案,

|x| + |y|

相比之下,在上面提出的解决方案中,切换区域基于max的值,其由下面的阴影方块表示。它确保两个区域连接得很好。

enter image description here

此外,上面的代码没有分支,这可能更有效。请考虑{<1}}和abs先验需要分支的操作​​,通常会有专门的汇编指令。出于这个原因,我认为这种方法优于另一种解决方案,即通过将nearlyEqual切换为diff < relth来修复迈克尔的diff < eps * relth,这样就可以产生相同的反应模式。

在相对和绝对比较之间切换的位置?

这些模式之间的切换是围绕relth进行的,在接受的答案中将其视为FLT_MIN。这个选择意味着float32的表示限制了浮点数的精度。

这并不总是有意义的。例如,如果您比较的数字是减法的结果,那么FLT_EPSILON范围内的某些内容可能更有意义。如果它们是减去数字的平方根,则数值不精确可能更高。

当您考虑将浮点与0进行比较时,这是显而易见的。在这里,任何相对比较都将失败,因为|x - 0| / (|x| + 0) = 1。因此,当x处于计算不精确的顺序时,比较需要切换到绝对模式 - 并且很少低于FLT_MIN

这就是上面引入relth参数的原因。

此外,如果不将relthepsilon相乘,则此参数的解释很简单,并且对应于我们对这些数字所期望的数值精度级别。

数学隆隆声

(此处主要是为了我自己的乐趣)

更一般地说,我假设一个行为良好的浮点比较运算符=~应该具有一些基本属性。

以下是相当明显的:

  • 自我平等: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]Ɛ参数化的平等关系系列relth时,还可以添加

  • 如果Ɛ1 < Ɛ2a =~[Ɛ1,t] b隐含a =~[Ɛ2,t] b(给定容差的等式意味着在更高的容忍度下相等)
  • 如果t1 < t2a =~[Ɛ,t1] b暗示a =~[Ɛ,t2] b(给定不精确的等式意味着在更高的不精确度下相等)

建议的解决方案也验证了这些。

答案 4 :(得分:3)

我想在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)
}

答案 5 :(得分:1)

Michael Borgwardt对PHP的改编&amp; bosonix的回答:

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;
            }
        }
    }
}

答案 6 :(得分:0)

标准建议是使用一些小的“epsilon”值(可能根据您的应用程序选择),并考虑彼此的epsilon内的浮点数相等。例如

之类的东西
#define EPSILON 0.00000001

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

更完整的答案很复杂,因为浮点错误极其微妙且令人困惑。如果您真正关心任何精确意义上的平等,那么您可能正在寻求一种不涉及浮点的解决方案。

答案 7 :(得分: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);
}

思考?我仍然需要研究一个大于,小于一个。

答案 8 :(得分:0)

您应该问自己为什么要比较这些数字。如果您知道比较的目的,那么您还应该知道所需的数字准确性。在每种情况和每种应用程序上下文中这都是不同的。但是在几乎所有实际情况下,都必须具有 绝对 精度。很少会应用相对精度。

举个例子:如果您的目标是在屏幕上绘制图形,那么您可能希望浮点值在映射到屏幕上的相同像素时比较相等。如果屏幕尺寸为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;
}

答案 9 :(得分: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 函数.

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

如果有原因,日志测试对各种大小的数字的简单比较无效,请在评论中说明原因。

答案 10 :(得分:-1)

比较平等/不平等的双精度的最佳方法是获取差异的绝对值,并将其与足够小(取决于您的上下文)值进行比较。

double eps = 0.000000001; //for instance

double a = someCalc1();
double b = someCalc2();

double diff = Math.abs(a - b);
if (diff < eps) {
    //equal
}

答案 11 :(得分:-1)

您需要考虑截断错误是相对错误。两个数字大致相等,如果它们的差异与它们的ulp(最后一个单位)一样大。

但是,如果你进行浮点计算,你的错误可能会随着每次操作而增加(尤其是减法!),因此你的容错需要相应增加。