浮点比较再访(Floating point comparison revisited)

2019-07-05 17:14发布

这个话题已经提出了很多次在计算器上,但我相信这是一个新的起飞。 是的,我已经阅读布鲁斯道森的文章和什么每台计算机科学家应该知道关于浮点运算和这个漂亮的答案 。

据我了解,一个典型的系统上有平等比较浮点数时,四个基本问题:

  1. 浮点计算不准确
  2. 无论ab是“小”取决于规模ab
  3. 是否ab为“小”取决于类型ab (例如浮动,双,长双)
  4. 浮点通常具有+ -infinity,NaN和非规范化表示,其中任一种可以与幼稚制剂干扰

这个答案 -又名。 “谷歌的做法” - 似乎是受欢迎。 它处理所有的棘手案件。 而且它的规模比较非常精确,检查两个值是否固定数量的内ULPS对方。 因此,例如,一个非常大的数量的比较“几乎相等”无穷大。

然而:

  • 这是非常混乱的,在我看来。
  • 这不是特别是便携式,上内部表示严重依赖,采用了联合从浮法等读出的位
  • 它只能处理单精度和双精度IEEE 754(尤其是没有长期的x86双)

我想类似的东西,但使用标准C ++和处理长一倍。 通过“标准”,我的意思是C ++ 03如果可能的话,如果需要C ++ 11。

这里是我的尝试。

#include <cmath>
#include <limits>
#include <algorithm>

namespace {
// Local version of frexp() that handles infinities specially.
template<typename T>
T my_frexp(const T num, int *exp)
{
    typedef std::numeric_limits<T> limits;

    // Treat +-infinity as +-(2^max_exponent).
    if (std::abs(num) > limits::max())
    {
        *exp = limits::max_exponent + 1;
        return std::copysign(0.5, num);
    }
    else return std::frexp(num, exp);
}
}

template<typename T>
bool almostEqual(const T a, const T b, const unsigned ulps=4)
{
    // Handle NaN.
    if (std::isnan(a) || std::isnan(b))
        return false;

    typedef std::numeric_limits<T> limits;

    // Handle very small and exactly equal values.
    if (std::abs(a-b) <= ulps * limits::denorm_min())
        return true;

    // frexp() does the wrong thing for zero.  But if we get this far
    // and either number is zero, then the other is too big, so just
    // handle that now.
    if (a == 0 || b == 0)
        return false;

    // Break the numbers into significand and exponent, sorting them by
    // exponent.
    int min_exp, max_exp;
    T min_frac = my_frexp(a, &min_exp);
    T max_frac = my_frexp(b, &max_exp);
    if (min_exp > max_exp)
    {
        std::swap(min_frac, max_frac);
        std::swap(min_exp, max_exp);
    }

    // Convert the smaller to the scale of the larger by adjusting its
    // significand.
    const T scaled_min_frac = std::ldexp(min_frac, min_exp-max_exp);

    // Since the significands are now in the same scale, and the larger
    // is in the range [0.5, 1), 1 ulp is just epsilon/2.
    return std::abs(max_frac-scaled_min_frac) <= ulps * limits::epsilon() / 2;
}

我要求这个代码(一)处理所有的相关的情况下,(B)做同样的事情作为谷歌实施IEEE-754单精度和双精度,以及(c)是完全标准的C ++。

一个或多个这些索赔几乎可以肯定是错误的。 我会接受这样的证明,最好用固定任何回答。 一个好的答案应该包括一个或多个:

  • 具体投入的不止不同ulps在最后的地方单位,但该函数返回true(较大的差别,越好)
  • 具体投入最多相差ulps在最后的地方单位,但该函数返回false(该差值越小越好)
  • (S)我已经错过任何情况下,
  • 任何方式使这一代码依赖于不确定的行为,或者根据实现定义的行为中断。 (如果可能的话,请引用相关的规范。)
  • 修正了什么问题(S)你识别
  • 任何方式简化代码而不被破坏。

我想订一个不平凡的奖金在这个问题上。

Answer 1:

“几乎等于”是不是一个好功能

4是不是一个合适的值:您指向状态答案“因此,4应该够正常使用”,但包含了这种说法没有依据。 事实上,也有在通过不同的方式在浮点计算的数字可以通过许多不同的ULP即使如果通过精确的数学计算,他们就等于普通的情况。 因此,应该有公差没有默认值; 每个用户应该被要求提供自己的,希望基于其代码透彻的分析。

至于为什么4 ULP的默认值是坏的例子,考虑1./49*49-1 。 在数学上精确的结果为0,但计算的结果(64位IEEE 754二进制)是-0x1p-53,超过了准确结果的1e307 ULP和几乎1E16 ULP计算结果的误差。

有时,没有值是适当的:在一些情况下,容差不能相对于所述的值被比较,既不是数学上精确的相对公差,也不是一个量化的ULP容差。 例如,在一个FFT几乎每一个输出值是由几乎所有的输入值的影响,并且在任何一个元件中的错误与其他元素的大小。 一个“几乎等于”程序必须提供有关潜在错误信息的其他方面。

“几乎等于”有穷的数学特性:这显示了“几乎等于”的缺点之一:缩放更改结果。 下面的代码打印1和0。

double x0 = 1.1;
double x1 = 1.1 + 3*0x1p-52;
std::cout << almostEqual(x0, x1) << "\n";
x0 *= .8;
x1 *= .8;
std::cout << almostEqual(x0, x1) << "\n";

另一个失败的是,它是不可传递的; almostEqual(a, b)almostEqual(b, c)并不意味着almostEqual(a, c)

在极端情况下虫虫

almostEqual(1.f, 1.f/11, 0x745d17)错误地返回1。

1.F / 11是0x1.745d18p-4。 从1中减去该(这是0x10p-4)产生0xe.8ba2e8p-4。 由于1的ULP是0x1p-23,即0xe.8ba2e8p19 ULP = 0xe8ba2e.8 / 2 ULP(移位20个比特和除以2,网19位)= 0x745d17.4 ULP。 这超过了0x745d17规定的公差,所以正确答案应该是0。

此错误是由四舍五入造成max_frac-scaled_min_frac

从这个问题的一个简单的逃避是指定ulps必须小于.5/limits::epsilon 。 然后舍入发生在max_frac-scaled_min_frac仅当差(四舍五入即使)超过ulps ; 如果该差小于,减法是精确的,由Sterbenz引理。

有一个关于建议使用long double纠正这一点。 然而, long double也不会纠正。 考虑与ULPS组比较1和-0x1p-149f〜1 /限制::小量。 除非你的有效数有149位时,减法结果舍入到1,其是小于或等于1 /限制::小量ULP。 然而,数学上的差异显然超过1。

未成年人注意

表达factor * limits::epsilon / 2因子转换为浮点型,这会导致舍入误差对于不是精确表示因子的大值。 可能的是,该例程不旨在与这种大的值(百万ULPS的浮法)使用,所以这应该被指定为在常规的限制,而不是一个错误。



Answer 2:

简化:你可以通过丢弃非有限的情况下,首先,一起避免my_frexp:

if( ! std::isfinite(a) || ! std::isfinite(b) )
    return a == b;

看来,ISFINITE是在C ++ 11至少

EDIT然而,如果目的是使limits::infinity() 1的ULP内limits::max()
那么上述简化不成立,但不应my_frexp()的返回limits::max_exponent+1*exp ,而不是MAX_EXPONENT + 2?



Answer 3:

适应未来发展 :如果你想延长这种比较,以十进制浮点http://en.wikipedia.org/wiki/Decimal64_floating-point_format在未来,假设ldexp()和frexp()将处理这种类型的与正确的基数,然后striclty讲,0.5 return std::copysign(0.5, num); 应改为T(1)/limits::radix() -或std::ldexp(T(1),-1)或东西...(我无法找到STD方便常数:: numeric_limits)

编辑作为尼莫评论,即ldexp和frexp将使用正确的FLOAT_RADIX的假设是错误的,他们坚持使用2 ...

因此,面向未来的便携版本也应该使用:

  • std::scalbn(x,n)的代替std::ldexp(x,n)

  • exp=std::ilogb(std::abs(x)),y=std::scalbn(x,-exp)代替y=frexp(x,&exp)

  • 现在,上面的Y为[1,FLOAT_RADIX)代替[T(1)/ Float_Radix,1),返回copysign(T(1),num)代替0.5 my_frexp无限的情况下,并测试ulps*limits::epsilon()代替ULPS *小量()/ 2

这还需要一个标准> = C ++ 11



文章来源: Floating point comparison revisited