69

私の質問は浮動小数点精度に関するものではありません。となぜEquals()違うのかについてです==

.1f + .2f == .3ffalse(while.1m + .2m == .3mが) である理由がわかりましたtrue
それ==は参照であり、.Equals()値の比較です。(編集:これには他にもあることを知っています。)

しかし、なぜ(.1f + .2f).Equals(.3f) true、ながら、(.1d+.2d).Equals(.3d)まだfalseですか?

 .1f + .2f == .3f;              // false
(.1f + .2f).Equals(.3f);        // true
(.1d + .2d).Equals(.3d);        // false
4

6 に答える 6

135

質問は紛らわしい表現です。それを多くの小さな質問に分解してみましょう。

浮動小数点演算で、10 分の 1 と 10 分の 2 が常に 10 分の 3 に等しくないのはなぜですか?

例え話をしましょう。すべての数値が正確に小数点以下 5 桁に四捨五入される数学システムがあるとします。次のように言うとします。

x = 1.00000 / 3.00000;

x は 0.33333 だと思いますよね?それは、私たちのシステムで本当の答えに最も近い数字だからです。今あなたが言ったとしましょう

y = 2.00000 / 3.00000;

y は 0.66667 だと思いますよね?繰り返しますが、それは私たちのシステムで本当の答えに最も近い数字だからです。0.66666 は 0.66667よりも 3 分の 2 から離れています。

最初のケースでは切り捨て、2 番目のケースでは切り上げていることに注意してください。

今私たちが言うとき

q = x + x + x + x;
r = y + x + x;
s = y + y;

私たちは何を得ますか?正確な計算を行うと、これらのそれぞれは明らかに 4/3 になり、すべて等しくなります。しかし、それらは等しくありません。1.33333 は私たちのシステムで 3 分の 4 に最も近い数ですが、その値を持つのは r だけです。

q は 1.33332 です -- x が少し小さかったため、すべての加算がそのエラーを蓄積し、最終結果はかなり小さすぎます。同様に、s は大きすぎます。y が少し大きすぎたため、1.33334 です。y の大きすぎることは x の小さすぎることによって相殺され、結果は正しい結果になるため、r は正しい答えを取得します。

精度の桁数は、エラーの大きさと方向に影響を与えますか?

はい; 精度が高いほど誤差の大きさは小さくなりますが、計算で誤差が原因で損失が生じるか利益が生じるかが変わる可能性があります。例えば:

b = 4.00000 / 7.00000;

b は 0.57143 になります。これは、0.571428571 の真の値から切り上げられます... 8 つの場所に行った場合、0.57142857 になります。これは、はるかに小さい誤差の大きさですが、反対方向です。切り捨てました。

精度を変更すると、個々の計算でエラーが増加するか減少するかが変わる可能性があるため、これにより、特定の集計計算のエラーが互いに補強し合うか、互いに打ち消し合うかが変わる可能性があります。最終的な結果は、精度の低い計算では運が良く、エラーの方向が異なるため、精度の低い計算の方が精度の高い計算よりも「真の」結果に近い場合があるということです。

より高い精度で計算を行うと、常に真の答えに近い答えが得られると予想されますが、この議論はそうではないことを示しています. これは、float での計算では「正しい」答えが得られることがあるのに、倍精度 (2 倍の精度を持つ) での計算では「間違った」答えが得られることがある理由を説明していますね。

はい、これはまさにあなたの例で起こっていることですが、5桁の10進精度の代わりに2進精度の特定の桁数があることを除いて. 3 分の 1 を 5 桁または任意の有限数の 10 進数で正確に表すことができないのと同様に、0.1、0.2、および 0.3 を任意の有限数の 2 進数で正確に表すことはできません。それらのいくつかは切り上げられ、いくつかは切り捨てられ、それらの追加がエラーを増加させるか、エラーをキャンセルするかは、各システムに含まれる2 進数の数の特定の詳細に依存します。つまり、精度が変わると答えが変わる可能性があります良くも悪くも。一般に、精度が高いほど、答えは真の答えに近づきますが、常にそうとは限りません。

float と double が 2 進数を使用している場合、どうすれば正確な 10 進算術計算を取得できますか?

正確な小数計算が必要な場合は、decimal型を使用してください。2 進数ではなく、10 進数を使用します。あなたが支払う代償は、それがかなり大きくて遅いということです. もちろん、すでに見てきたように、3 分の 1 や 7 分の 4 などの分数は正確に表現されません。ただし、実際には小数である分数は、最大約 29 桁まで、ゼロ エラーで表されます。

OK、すべての浮動小数点スキームが表現エラーによる不正確さを導入すること、およびそれらの不正確さが計算で使用される精度のビット数に基づいて蓄積または相殺される場合があることを受け入れます。少なくとも、これらの不正確さが一貫しているという保証はありますか?

いいえ、float や double についてはそのような保証はありません。コンパイラとランタイムはどちらも、仕様で要求されているよりも高い精度で浮動小数点計算を実行することが許可されています。特に、コンパイラとランタイムは、64 ビット、80 ビット、128 ビット、または好きな 32 より大きい任意のビット数で、単精度 (32 ビット) 演算を実行できます。

コンパイラとランタイムは、そうすることが許可されていますが、その時点ではそのように感じています。それらは、マシンごと、実行ごとなどで一貫している必要はありません。これにより計算がより正確になるだけなので、これはバグとは見なされません。特徴です。予測どおりに動作するプログラムを作成することを非常に困難にする機能ですが、それでも機能です。

つまり、リテラル 0.1 + 0.2 のように、コンパイル時に実行される計算は、変数を使用して実行時に実行される同じ計算とは異なる結果をもたらす可能性があるということですか?

うん。

0.1 + 0.2 == 0.3と の結果を比較するのはどう(0.1 + 0.2).Equals(0.3)ですか?

最初のものはコンパイラによって計算され、2 つ目はランタイムによって計算されるため、気まぐれで仕様で必要とされるよりも高い精度を任意に使用することが許可されていると言いました。はい、それらは異なる結果をもたらす可能性があります。そのうちの 1 人は 64 ビット精度でのみ計算を行うことを選択し、もう 1 人は計算の一部またはすべてに 80 ビットまたは 128 ビット精度を選択して、異なる答えを得ます。

ここでちょっと待ってください。0.1 + 0.2 == 0.3だけでなく、 とは異なる可能性があると言っています(0.1 + 0.2).Equals(0.3)。あなたは0.1 + 0.2 == 0.3、コンパイラの気まぐれで完全に真または偽になるように計算できると言っています。火曜日に true を生成し、木曜日に false を生成したり、あるマシンで true を生成し、別のマシンで false を生成したり、式が同じプログラムに 2 回出現した場合に true と false の両方を生成したりできます。この式は、何らかの理由でいずれかの値を持つことができます。ここでは、コンパイラは完全に信頼できないことが許されています。

正しい。

これが通常 C# コンパイラ チームに報告される方法は、誰かが、デバッグ モードでコンパイルすると true を生成し、リリース モードでコンパイルすると false を生成する何らかの式を持っているということです。これは、デバッグおよびリリース コードの生成によってレジスタ割り当てスキームが変更されるため、これが発生する最も一般的な状況です。ただし、コンパイラは、true または false を選択する限り、この式で好きなことを行うことができます(たとえば、コンパイル時エラーを生成することはできません。)

これは狂気です。

正しい。

この混乱の責任は誰にあるのでしょうか?

私ではありません、それは確かです。

インテルは、一貫した結果を得るにははるかに高価な浮動小数点演算チップを作成することを決定しました。どの操作を登録するか、どの操作をスタックに保持するかについてのコンパイラーの小さな選択は、結果に大きな違いをもたらす可能性があります。

一貫した結果を得るにはどうすればよいですか?

decimal前に言ったように、タイプを使用してください。または、すべての計算を整数で行います。

double または float を使用する必要があります。一貫した結果を促すために何かできることはありますか?

はい。結果を静的フィールドクラスのインスタンス フィールド、またはfloat または double 型の配列要素に格納すると、32 ビットまたは 64 ビットの精度に切り捨てられることが保証されます。(この保証は、ローカルまたは仮パラメーターへのストアに対して明示的に行われません。) また、既にその型である式に対して実行時キャストを実行(float)する(double)と、コンパイラーは、結果を強制的に切り捨てる特別なコードを出力します。フィールドまたは配列要素に割り当てられていました。(コンパイル時に実行されるキャスト、つまり、定数式に対するキャストは、そうすることが保証されていません。)

最後のポイントを明確にするために、C#言語仕様はそれらの保証を行っていますか?

いいえ。ランタイムは、配列またはフィールドの切り捨てへの格納を保証します。C# の仕様では、ID キャストが切り捨てられることは保証されていませんが、Microsoft の実装には、コンパイラのすべての新しいバージョンがこの動作をすることを確認する回帰テストがあります。

この件に関して言語仕様が述べなければならないのは、実装の裁量で浮動小数点演算をより高い精度で実行できるということだけです。

于 2013-02-27T16:54:01.963 に答える
8

あなたが書くとき

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;

実際には、これらは厳密0.1には ,0.2と ではありません0.3。ILコードから;

  IL_0001:  ldc.r8     0.10000000000000001
  IL_000a:  stloc.0
  IL_000b:  ldc.r8     0.20000000000000001
  IL_0014:  stloc.1
  IL_0015:  ldc.r8     0.29999999999999999

SO ポインティングには ( .NET での decimal、float、double の違い?および.NETでの浮動小数点エラーの処理) のような多くの質問がありますが、次のようなクールな記事を読むことをお勧めします。

What Every Computer Scientist Should Know About Floating-Point Arithmetic

うーん、レッピーの言ったことはもっと論理的だ。実際の状況はここにあり、完全に/またはに依存します。compilercomputercpu

leppie コードに基づいて、このコードは私のVisual Studio 2010Linqpadで動作し、結果としてTrue/になりますが、 ideone.comFalseで試してみると、結果は/になります。TrueTrue

デモを確認してください。

ヒント: 私が書いたときConsole.WriteLine(.1f + .2f == .3f);、Resharper が警告してくれました。

等価演算子による浮動小数点数の比較。値を丸める際に精度が失われる可能性があります。

ここに画像の説明を入力

于 2013-02-27T16:35:54.620 に答える
6

コメントで述べたように、これはコンパイラが一定の伝播を行い、より高い精度で計算を実行するためです (これは CPU に依存していると思います)。

  var f1 = .1f + .2f;
  var f2 = .3f;
  Console.WriteLine(f1 == f2); // prints true (same as Equals)
  Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)

.1f+.2f==.3f@Caramiriel は、それが IL のように発行されることも指摘しているfalseため、コンパイラはコンパイル時に計算を行いました。

定数折りたたみ/伝播コンパイラの最適化を確認するには

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false
于 2013-02-27T16:41:52.050 に答える
2

FWIW 次のテスト パス

float x = 0.1f + 0.2f;
float result = 0.3f;
bool isTrue = x.Equals(result);
bool isTrue2 = x == result;
Assert.IsTrue(isTrue);
Assert.IsTrue(isTrue2);

したがって、問題は実際にはこの行にあります

0.1f + 0.2f==0.3f

述べたように、おそらくコンパイラ/ PC固有のものです

ほとんどの人は、間違った角度からこの質問に飛びついていると思います。

アップデート:

私が思うもう一つの興味深いテスト

const float f1 = .1f + .2f;
const float f2 = .3f;
Assert.AreEqual(f1, f2); passes
Assert.IsTrue(f1==f2); doesnt pass

単一の等価実装:

public bool Equals(float obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}
于 2013-02-27T16:44:15.857 に答える
0

==正確なフロート値を比較することです。

Equalstrue または false を返すブールメソッドです。具体的な実装は異なる場合があります。

于 2013-02-27T17:32:49.933 に答える