如何进行浮点数比较?

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

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


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

And then in other places I may need to do equality:

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

我将其标记为语言不可知论者,因为我感兴趣的是,无论我使用什么语言,我都可以实现这一点。

110901 次浏览

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

#define EPSILON 0.00000001


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

一个更完整的答案是复杂的,因为浮点错误是非常微妙和令人困惑的推理。如果您真正关心任何精确意义上的相等,那么您可能正在寻找一个不涉及浮点数的解决方案。

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

double eps = 0.000000001; //for instance


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


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

你需要考虑到截尾误差是相对的。如果两个数字之间的差异和它们的 ulp 一样大(单位在最后) ,那么两个数字大致相等。

但是,如果进行浮点计算,则每次操作(尤其是。小心减法!)因此,您的错误容忍度需要相应地增加。

我试着根据上面的注释编写一个等式函数:

Edit: Change from Math.Max(a, b) to 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);
}

Thoughts? I still need to work out a greater than, and a less than as well.

Comparing for greater/smaller is not really a problem unless you're working right at the edge of the float/double precision limit.

对于“模糊等于”的比较,这个(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,两个非常小的值与零相反,或者无穷大。

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

在任何情况下,可能没有任何解决方案是完美的所有应用程序。理想情况下,您可以开发/调整您自己的测试套件,以覆盖您的实际用例。

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

The fabs--absolute value-- takes care of if they are essentially equal.

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

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

改编自 Michael Borgwardt & 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;
}
}
}
}

我对快速浮点数比较的想法

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

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

请给我图表?

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

第一种是 relative模式,其中 xy之间的差异被认为是相对于它们的振幅 |x| + |y|。当绘制2D 图时,它给出以下配置文件,其中绿色表示 xy相等。(为了便于说明,我参加了0.5的 epsilon考试)。

enter image description here

相对模式用于“正常”或“足够大”的浮点值。

第二个模式是 绝对的模式,我们只是将它们的差异与一个固定的数字进行比较。它给出了以下配置文件(同样是 epsilon为0.5,abs_th为1)。

enter image description here

This absolute mode of comparison is what is used for "tiny" floating point values.

现在的问题是,我们如何把这两种反应模式联系起来。

在 MichaelBorgwardt 的回答中,开关是基于 diff的值,它应该低于 abs_th(在他的回答中是 Float.MIN_NORMAL)。这个开关区域如下图所示。

enter image description here

因为 abs_th * epsilonabs_th小,绿色斑块不能粘在一起,这反过来又给了解决方案一个不好的性质: 我们可以找到三个数字,比如 x < y_1 < y_2x == y2但是 x != 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来修复迈克尔的 nearlyEqual,这将产生本质上相同的响应模式。

如何在相对比较和绝对比较之间切换?

这些模式之间的切换是在 abs_th附近进行的,在公认的答案中,abs_th被认为是 FLT_MIN。这个选择意味着 float32的表示限制了浮点数的精度。

这并不总是有意义的。例如,如果你比较的数字是减法的结果,也许在 FLT_EPSILON范围内的东西更有意义。如果它们是减去的数的平方根,数值的不精确度可能更高。

It is rather obvious when you consider comparing a floating point with 0. Here, any relative comparison will fail, because |x - 0| / (|x| + 0) = 1. So the comparison needs to switch to absolute mode when x is on the order of the imprecision of your computation -- and rarely is it as low as FLT_MIN.

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

此外,由于没有乘以 abs_thepsilon,这个参数的解释是简单的,并对应于数值精度的水平,我们期望对这些数字。

数学轰鸣

(留在这里主要是为了我自己的乐趣)

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

The following are rather obvious:

  • self-equality: 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(也许不那么明显的论点)。

这些性质已经对可能的近等式函数提供了强大的约束。上面提出的函数对它们进行了验证。也许缺少一个或几个其他明显的属性。

当我们把 =~看作是一个由 Ɛabs_th参数化的等式关系 =~[Ɛ,t]家族时,我们也可以加上

  • 如果 Ɛ1 < Ɛ2,那么 a =~[Ɛ1,t] b意味着 a =~[Ɛ2,t] b(公平的给定公差意味着公平的更高的公差)
  • if t1 < t2 then a =~[Ɛ,t1] b implies a =~[Ɛ,t2] b (equality for a given imprecision implies equality at a higher imprecision)

提出的解决方案也验证了这些。

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

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

我想出了一个简单的方法来调整 ε 的大小以适应被比较的数字的大小。因此,不要使用:

iif(abs(a - b) < 1e-6, "equal", "not")

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

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

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

I did a search to see if others have used this method and I didn't find anything. I tested it in my application and it seems to be working well. So it seems to be a method that is adequate for contexts that don't require the complexity of the other answers. But I wonder if it has a problem I haven't thought of since no one else seems to be using it.

如果日志测试对于不同大小的数字的简单比较是无效的,请在注释中说明原因。