26

このトピックは StackOverflow で何度も取り上げられてきましたが、これは新しい見方だと思います。はい、私はBruce Dawson の記事What Every Computer Scientist Should Know About Floating-Point Arithmeticthis nice answerを読みました。

私が理解しているように、典型的なシステムでは、浮動小数点数が等しいかどうかを比較するときに 4 つの基本的な問題があります。

  1. 浮動小数点の計算は正確ではありません
  2. が「小さい」かどうかは、とa-bのスケールに依存します。ab
  3. a-bが「小さい」かどうかは、 aandの型b(例: float、double、long double)によって異なります。
  4. 通常、浮動小数点には +-無限大、NaN、および非正規化表現があり、これらはいずれも単純な定式化を妨げる可能性があります

この答え- 別名。「Google アプローチ」 -- 人気があるようです。それはすべてのトリッキーなケースを処理します。また、比較を非常に正確にスケーリングし、2 つの値が互いに一定数のULP内にあるかどうかをチェックします。したがって、たとえば、非常に大きな数は、「ほぼ等しい」と無限大を比較します。

でも:

  • 私の意見では、それは非常に面倒です。
  • 内部表現に大きく依存し、ユニオンを使用して float からビットを読み取るなど、特に移植性はありません。
  • 単精度と倍精度の IEEE 754 のみを処理します (特に、x86 long double は処理しません)。

似たようなものが欲しいのですが、標準の C++ を使用し、長い double を処理します。「標準」とは、可能であれば 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;
}

このコードは、(a) 関連するすべてのケースを処理し、(b) IEEE-754 単精度および倍精度の Google 実装と同じことを行い、(c) 完全に標準の C++ であると主張します。

これらの主張の 1 つまたは複数は、ほぼ確実に間違っています。できれば修正を加えて、そのようなことを示す回答を受け入れます。適切な回答には、次の 1 つ以上が含まれている必要があります。

  • 最後の場所の単位よりもulps大きく異なるが、この関数が true を返す特定の入力 (差が大きいほど良い)
  • ulps最終位の単位までは異なるが、この関数が false を返す特定の入力(差が小さいほど良い)
  • 私が見逃したすべてのケース
  • このコードが未定義の動作に依存する方法、または実装定義の動作に応じて壊れる方法。(可能であれば、関連する仕様を引用してください。)
  • 特定した問題の修正
  • コードを壊さずに単純化する方法

私は、この問題に重大な報奨金を与えるつもりです。

4

4 に答える 4

25

「ほぼ等しい」は良い関数ではない

4 は適切な値ではありません:あなたが指摘した回答は、「したがって、通常の使用には 4 で十分なはずです」と述べていますが、その主張の根拠は含まれていません。実際、正確な数学によって計算された場合に等しい場合でも、さまざまな方法で浮動小数点で計算された数値が多くの ULP によって異なる可能性がある通常の状況があります。したがって、許容値のデフォルト値はありません。各ユーザーは、できればコードの徹底的な分析に基づいて、独自のものを提供する必要があります。

デフォルトの 4 ULP が良くない理由の例として、 を考えてみましょう1./49*49-1。数学的に正確な結果は 0 ですが、計算された結果 (64 ビット IEEE 754 バイナリ) は 2 -53であり、正確な結果の 10 307 ULP を超え、計算結果のほぼ 10 16 ULPを超えるエラーです。

適切な値がない場合があります。場合によっては、許容値が比較対象の値に相対的でなく、数学的に正確な相対許容値でも、量子化された ULP 許容値でもない場合があります。たとえば、FFT のほぼすべての出力値はほぼすべての入力値の影響を受け、1 つの要素の誤差は他の要素の大きさに関連しています。「ほぼ等しい」ルーチンには、潜在的なエラーに関する情報を含む追加のコンテキストを提供する必要があります。

「ほぼ等しい」には不十分な数学的特性があります。これは、「ほぼ等しい」の欠点の 1 つを示しています。スケーリングによって結果が変わります。以下のコードは 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";

もう 1 つの欠点は、それが推移的でないことです。almostEqual(a, b)を意味するものではありalmostEqual(b, c)ませんalmostEqual(a, c)

極端な場合のバグ

almostEqual(1.f, 1.f/11, 0x745d17)誤って 1 を返します。

1.f/11 は 0x1.745d18p-4 (16 進浮動小数点表記、つまり 1.745d18 16 •2 −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 の Lemma により、減算は正確です。

long doubleこれを修正するために使用することについての提案がありました。ただし、long doubleこれは修正しません。1 と -0x1p-149f を にulps設定して比較することを検討してください1/limits::epsilon1/limits::epsilon仮数が 149 ビットでない限り、減算結果は 1 に丸められ、 ULP以下になります。それでも、数学的な差は明らかに 1 を超えています。

マイナーノート

式は浮動小数点型にfactor * limits::epsilon / 2変換されるため、正確に表現できないfactor大きな値の丸め誤差が発生します。factorおそらく、このルーチンは、このような大きな値 (数百万の ULP float) で使用することを意図していないため、これはバグではなく、ルーチンの制限として指定する必要があります。

于 2012-12-18T20:57:27.963 に答える
2

単純化:最初に非有限のケースをまとめて破棄することで、my_frexp を回避できます。

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

isfinite は少なくとも C++11 にあるようです

編集しかし、意図がlimits::infinity()1 ulp以内にある場合limits::max()
、上記の単純化は成り立ちませんが、 my_frexp()は max_exponent+2 ではなくに返さlimits::max_exponent+1れるべきではありませんか?*exp

于 2012-12-18T23:03:38.153 に答える
0

将来の証明: 将来、このような比較を 10 進浮動小数点数http://en.wikipedia.org/wiki/Decimal64_floating-point_formatに拡張したい場合、ldexp() と frexp() がそのような型を正しい基数で処理すると仮定します。 、厳密に言えば、0.5インチを-または何かreturn std::copysign(0.5, num);に置き換える必要があります...(std::numeric_limitsに便利な定数が見つかりませんでした)T(1)/limits::radix()std::ldexp(T(1),-1)

EDIT Nemo がコメントしたように、ldexp と frexp が正しい FLOAT_RADIX を使用するという仮定は誤りであり、2 に固執します...

したがって、Future Proof のポータブル バージョンでも次を使用する必要があります。

  • std::scalbn(x,n)それ以外のstd::ldexp(x,n)

  • exp=std::ilogb(std::abs(x)),y=std::scalbn(x,-exp)それ以外のy=frexp(x,&exp)

  • 上記の y は [T(1)/Float_Radix,1) ではなく [1,FLOAT_RADIX) になり、my_frexpcopysign(T(1),num)の無限の場合は 0.5 の代わりに戻りulps*limits::epsilon()、ulps*epsilon()/2 の代わりにテストします。

標準の >= C++11 も必要です

于 2012-12-19T00:58:59.293 に答える