浮動小数点値を比較することはどれほど危険ですか?


391

解像度に依存しない座標系のため、私はUIKit用途を知ってCGFloatいます。

しかし、私は、例えばかどうかを確認するたびにframe.origin.xされ0、それは私が病気に感じさせます。

if (theView.frame.origin.x == 0) {
    // do important operation
}

ではないCGFloatと比較する際に偽陽性に弱いですか==<=>=<>?これは浮動小数点であり0.0000000000041、たとえば不正確な問題があります。

されてObjective-C比較するときに内部的にこれを処理するか、ということが起こる可能origin.xゼロとして読み込むにはとは比較にならない0本当のよう?

回答:


466

まず、浮動小数点値の動作は「ランダム」ではありません。正確な比較は、実際の多くの使用法で有効であり、実際に使用できます。しかし、浮動小数点を使用する場合は、それがどのように機能するかを認識する必要があります。浮動小数点が実数のように機能すると仮定する側を誤ると、すぐに壊れるコードが得られます。浮動小数点の結果に大きなランダムファズが関連付けられていると想定する側(ここでのほとんどの回答が示唆するように)に誤りを犯すと、最初は機能しているように見えるが、最終的には大きなエラーと壊れたコーナーケースが発生するコードが得られます。

まず第一に、浮動小数点でプログラムしたいなら、これを読むべきです:

すべてのコンピューター科学者が浮動小数点演算について知っておくべきこと

はい、すべてお読みください。負担が大きすぎる場合は、整数/固定小数点を使用して、読み取る時間があるまで計算を行う必要があります。:-)

さて、そうは言っても、正確な浮動小数点比較の最大の問題は次のようになります。

  1. 値の多くは、あなたがソースに書き込み、またはにして読むことができるという事実scanfstrtod存在していない浮動小数点値として静かに最も近い近似値に変換します。これは、demon9733の答えが話していたものです。

  2. 実際の結果を表すのに十分な精度がないため、多くの結果が丸められるという事実。あなたはこれが追加されて見ることができる簡単な例x = 0x1fffffey = 1山車として。ここでxは、仮数部に24ビットの精度(ok)がありy、1ビットしかありませんが、それらを追加すると、それらのビットは重複する場所になく、結果には25ビットの精度が必要になります。代わりに、丸められます(0x2000000デフォルトの丸めモードに)。

  3. 正しい値のために無限に多くの場所が必要になるため、多くの結果が丸められるという事実。これには、1/3(無限に多くの場所をとる10進数でよく知られている)などの合理的な結果と、1/10(2の累乗ではないため、バイナリで無限に多くの場所をとる)の両方が含まれます。完璧な正方形ではないものの平方根のような不合理な結果も同様です。

  4. 二重丸め。一部のシステム(特にx86)では、浮動小数点式は公称型よりも高い精度で評価されます。つまり、上記のタイプの丸めのいずれかが発生すると、2つの丸めステップが発生します。最初に結果をより精度の高い型に丸め、次に最終型に丸めます。例として、1.49を整数(1)に丸めると10進数で何が起こるか、最初に小数第1位(1.5)に丸め、次にその結果を整数(2)に丸めるとどうなるかを考えます。コンパイラーの動作(特にバグの多い、GCCなどの非準拠コンパイラー)は予測できないため、これは実際には浮動小数点で処理するのが最も厄介な領域の1つです。

  5. 超越関数(trigexplog、など)が正しく丸められた結果を持つように指定されていません。結果は、最後の精度の場所(通常1ulpと呼ばれる)の1単位内で正確であると指定されているだけです。

浮動小数点コードを作成するときは、結果が不正確になる可能性のある数値をどのように処理しているかに留意し、それに応じて比較を行う必要があります。多くの場合、「イプシロン」と比較することは理にかなっていますが、そのイプシロンは、絶対定数ではなく、比較する数値大きさに基づいている必要があります。(絶対定数イプシロンが機能する場合、浮動小数点ではなく固定小数点がその仕事に適切なツールであることを強く示しています!)

編集:特に、マグニチュード相対イプシロンチェックは次のようになります。

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

はどこFLT_EPSILONからの定数であるかfloat.hDBL_EPSILONfor doublesまたはLDBL_EPSILONfor long doublesに置き換えKます)、計算の累積エラーがK最後の場所の単位によって確実に制限されるように選択した定数です(そして、エラーが発生したかどうかわからない場合バインドされた計算が正しく、計算で想定されている値Kよりも数倍大きくします)。

最後に、これを使用する場合FLT_EPSILON、非正規化には意味がないため、ゼロ付近で特別な注意が必要になる場合があることに注意してください。簡単な修正はそれを作ることです:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

DBL_MINダブルを使用する場合も同様に置き換えます。


25
fabs(x+y)xy(異なる)符号が異なる場合は問題があります。それでも、カーゴカルト比較の流れに対する良い答えです。
ダニエルフィッシャー

27
xy別の兆候がある場合は、問題ありません。右側は "小さすぎる"ものになりますが、xとのy符号が異なるため、とにかく等しいと比較すべきではありません。(それらが非正規であるほど小さいが、2番目のケースがそれをキャッチしない限り)
R .. GitHub ICE HELPING ICE

4
「特にバグの多い、GCCのような非準拠コンパイラの場合」というあなたの発言に興味があります。本当にGCCバグがあり、不適合ですか?
ニコラスOzimica

3
質問にはiOSのタグが付けられているため、Appleのコンパイラ(clangビルドとAppleのgccビルドの両方)は常にFLT_EVAL_METHOD = 0を使用しており、過剰な精度を持たないように完全に厳密にしようとしています。それに対する違反を見つけた場合は、バグレポートを提出してください。
スティーブンキャノン

17
「まず第一に、浮動小数点値はその動作において「ランダム」ではありません。正確な比較は、実際の多くの使用法で意味があり、実際に意味があります。」-たった2文で、すでに+1を獲得しています。これは、浮動小数点を扱うときに人々が行う最も厄介な誤解の1つです。
Christian Rau 2013

36

0はIEEE754浮動小数点数として正確に表現できるため(または、これまでに使用した他のfp数の実装を使用して)、0との比較はおそらく安全です。ただし、プログラムtheView.frame.origin.xが0であると信じるに足る理由があるが、計算で0であることを保証できない値(など)をプログラムが計算すると、かみつく可能性があります。

少し明確にするために、次のような計算:

areal = 0.0

(言語またはシステムが壊れていない限り)(areal == 0.0)がtrueを返すような値を作成しますが、

areal = 1.386 - 2.1*(0.66)

ではないかもしれない。

計算が0である値を生成することを保証できる場合(そして、それらが0であるべき値を生成するだけではない)、先に進んでfp値を0と比較できます。必要な程度まで自分自身を保証できない場合、「寛容な平等」という通常のアプローチに忠実に従う。

最悪の場合、fp値の不注意な比較は非常に危険になる可能性があります。航空電子工学、兵器のガイダンス、発電所の操作、車両のナビゲーション、計算が現実世界と出会うほとんどすべてのアプリケーションを考えてください。

Angry Birdsにとってはそれほど危険ではありません。


11
実際には、1.30 - 2*(0.65)ダブルスのように表されているため、あなたのコンパイラの実装は、754をIEEEた場合、明らかに0.0と評価されることを表現の完璧な例です0.651.30同じ仮数を持っているし、2つの乗算が正確に明らかです。
Pascal Cuoq 2013

7
これからまだ担当者がいるので、2番目のサンプルスニペットを変更しました。
ハイパフォーマンスマーク

22

他の人とは少し違う答えを出したいと思います。彼らは述べられているようにあなたの質問に答えるのに最適ですが、おそらくあなたが知る必要があるものやあなたの本当の問題が何であるかのためではありません。

グラフィックの浮動小数点は問題ありません!しかし、フロートを直接比較する必要はほとんどありません。なぜあなたはそれをする必要があるのですか?グラフィックはフロートを使用して間隔を定義します。そして、フロートが間隔内にあるかどうかの比較は、フロートによっても定義され、常に正確に定義され、正確である必要はなく、正確である必要もありません!ピクセル(間隔でもある!)を割り当てることができる限り、それはすべてのグラフィックの必要性です。

したがって、ポイントが[0..width [の範囲外であるかどうかをテストする場合は、これで十分です。包含を一貫して定義していることを確認してください。たとえば、常に内部を定義するのは(x> = 0 && x <幅)です。同じことが交差テストまたはヒットテストにも当てはまります。

ただし、たとえばウィンドウがドッキングされているかどうかを確認するために、グラフィック座標を何らかのフラグとして悪用している場合は、これを行わないでください。代わりに、グラフィックプレゼンテーションレイヤーとは別のブールフラグを使用してください。


13

ゼロに比較することができる限り、(上記の回答で述べたように)ゼロでは計算値ではなかったように、安全な操作です。これは、ゼロが浮動小数点で完全に表現可能な数値であるためです。

完全に表現可能な値と言えば、2の累乗の概念(単精度)で24ビットの範囲が得られます。したがって、1、2、4、.5、.25、.125は完全に表現可能です。すべての重要なビットが24ビットである限り、あなたは黄金です。したがって、10.625は正確に表すことができます。

これはすばらしいことですが、プレッシャーにさらされるとすぐにバラバラになります。2つのシナリオが思い浮かびます:1)計算が含まれる場合。sqrt(3)* sqrt(3)== 3であると信頼しないでください。それはそのようにはなりません。そして、他のいくつかの答えが示唆しているように、それはおそらくイプシロン内にはありません。2)2の累乗でない(NPOT)が含まれる場合。奇妙に聞こえるかもしれませんが、0.1は2進数の無限級数なので、このような数値を含む計算は最初から不正確になります。

(ああ、元の質問はゼロとの比較について述べています。-0.0も完全に有効な浮動小数点値であることを忘れないでください。)


11

[「正しい答え」は、選択よりも優れていKます。選択Kは、選択と同じようにアドホックになりますが、表示プロパティに基づいていないのでVISIBLE_SHIFT、選択Kは明白VISIBLE_SHIFTではありません。したがって、毒を選択するKか、選択しますVISIBLE_SHIFT。この回答は選択VISIBLE_SHIFTを支持し、次に選択の難しさを示していますK]

正確に丸め誤差があるため、論理演算では「正確な」値の比較を使用しないでください。ビジュアルディスプレイ上の特定の位置の場合、位置が0.0または0.0000000003であるかどうかは問題になりません。違いは目に見えません。したがって、ロジックは次のようになります。

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

ただし、結局、「目に見えない」ことは、ディスプレイのプロパティに依存します。ディスプレイの上限を設定できる場合(できるはずです)。次にVISIBLE_SHIFT、その上限の一部になるように選択します。

さて、「正しい答え」がかかっているKので、ピッキングを探りましょうK。上記の「正しい答え」は言う:

Kは選択した定数であり、計算の累積エラーが最後の場所でK単位によって確実に制限されるようにします(エラー境界の計算が正しいかどうかわからない場合は、Kを計算の数倍にしますそれがあるべきだと言う)

だから私たちは必要Kです。取得Kが難しく、直感的ではないVISIBLE_SHIFT場合は、私を選択するよりも、自分に合った方法を決定します。見つけるためKに、一連のK値を調べてどのように動作するかを確認できるテストプログラムを作成します。K「正しい答え」が使用できる場合、選択方法を明確にする必要があります。番号?

「正しい答え」の詳細として使用します。

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Kのすべての値を試してみましょう。

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

ああ、1e-13を「ゼロ」にしたい場合は、Kを1e16以上にしてください。

したがって、2つのオプションがあると思います。

  1. 私が提案したように、「イプシロン」の値のエンジニアリング判断を使用して、簡単なイプシロン計算を実行します。グラフィックスを実行していて、「ゼロ」が「目に見える変化」であることを意図している場合、視覚的資産(画像など)を調べて、イプシロンが何であるかを判断します。
  2. カーゴカルト以外の回答のリファレンスを読んで(その過程で博士号を取得する)まで、浮動小数点の計算を行わないでください。その後、直感的でない判断を使用してを選択しますK

10
解像度に依存しないことの1つの側面は、コンパイル時に「目に見える変化」が何であるかを確実に判断できないことです。スーパーHD画面で見えないものは、超低解像度の画面では非常にはっきりしているかもしれません。少なくとも画面サイズの関数にする必要があります。または別の名前を付けます。
ロメイン

1
しかし、少なくとも「目に見えるシフト」の選択は、簡単に理解できる表示(またはフレーム)プロパティに基づいKています。選択するのが難しく、直感的ではない<正解> とは異なります。
GoZoner

5

正しい質問:Cocoa Touchのポイントをどのように比較しますか?

正解:CGPointEqualToPoint()。

別の質問:2つの計算値は同じですか?

ここに投稿された答え:彼らはそうではありません。

それらが近いかどうかを確認するにはどうすればよいですか?それらが近いかどうかを確認したい場合は、CGPointEqualToPoint()を使用しないでください。ただし、近くにあるかどうかを確認しないでください。点が線を超えているかどうか、または点が球の内側にあるかどうかを確認するなど、現実の世界で意味のあることを行います。


4

前回C標準を確認したとき、倍精度浮動小数点演算(合計64ビット、仮数53ビット)がその精度以上の精度である必要はありませんでした。ただし、一部のハードウェアはより高い精度のレジスターで操作を実行する場合があり、要件は、(レジスターにロードされる数値の精度を超えて)下位ビットをクリアする必要がないことを意味すると解釈されました。したがって、最後に眠った人からレジスターに残ったものに応じて、このような比較の予期しない結果を得る可能性があります。

とはいえ、私が目にするたびにそれを抹消しようとする私の努力にもかかわらず、私が作業している服には、gccを使用してコンパイルされ、Linuxで実行されるCコードがたくさんあります。 。これは、gccが下位ビットをクリアしているためか、80ビットのレジスターが最近のコンピューターでこれらの操作に使用されていないのか、標準が変更されているのか、何なのか、私にはわかりません。誰かが章と節を引用できるかどうか知りたいのですが。


1

このようなコードを使用して、floatをゼロと比較できます。

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

これは、この場合のCGFloatには十分な0.1の精度と比較されます。


int保険なしでのキャストtheView.frame.origin.xは、その範囲内またはその近くでint、未定義の動作(UB)につながりintます。この場合、の範囲の1/100です。
chux-モニカの復活2017年

このように整数に変換する理由はまったくありません。chuxが言ったように、範囲外の値からUBの可能性があります。一部のアーキテクチャでは、これは浮動小数点で計算を行うよりも大幅に遅くなります。最後に、このように100を掛けると、0.1ではなく0.01の精度と比較されます。
Sneftel 2018

0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}


0

以下の比較関数を使用して、小数点以下の桁数を比較しています。

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

-6

各数値をオブジェクトとして宣言し、そのオブジェクトで3つのことを定義するのが正しいと思います。1)等値演算子。2)setAcceptableDifferenceメソッド。3)値自体。等値演算子は、2つの値の絶対差が許容範囲として設定された値より小さい場合にtrueを返します。

問題に合わせてオブジェクトをサブクラス化できます。たとえば、1〜2インチの金属の丸棒は、直径の差が0.0001インチ未満の場合、同じ直径と見なされます。したがって、パラメーター0.0001を指定してsetAcceptableDifferenceを呼び出し、次に等号演算子を自信を持って使用します。


1
これは良い答えではありません。まず、「対象物」全体では、問題を解決するために何も行いません。次に、「平等」の実際の実装は、実際には正しくありません。
トムスワーリー

3
トム、多分あなたは「対象物」についてもう一度考えるでしょう。高精度で表現された実数では、平等はめったに起こりません。しかし、平等という考え方は、それがあなたに合っている場合は調整されるかもしれません。オーバーライド可能な「ほぼ等しい」演算子があった方がいいでしょうが、そうではありません。
ジョンホワイト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.