web-dev-qa-db-ja.com

(.1f + .2f ==。3f)!=(.1f + .2f).Equals(.3f)なぜですか?

私の質問は、浮動小数点精度についてnotです。 Equals()が_==_と異なるのはそのためです。

_.1f + .2f == .3f_がfalseである理由を理解しています(_.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
_
67
LZW

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

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

類推をさせてください。すべての数値が小数点第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はシステムの4分の3に最も近い数値ですが、その値を持つのはrだけです。

qは1.33332です。xは少し小さかったため、追加するたびにエラーが累積し、最終結果はかなり小さすぎます。同様に、sは大きすぎます。 yが少し大きすぎたため、1.33334です。 yの大きすぎることはxの小さすぎることによってキャンセルされ、結果は正しい結果になるため、rは正しい答えを取得します。

精度の桁数は誤差の大きさと方向に影響しますか?

はい;精度を上げると、誤差の大きさは小さくなりますが、計算により誤差が原因で損失またはゲインが発生するかどうかが変わる可能性があります。例えば:

b = 4.00000 / 7.00000;

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

精度を変更すると、個々の計算での誤差が利得か損失かが変わるため、特定の集計計算の誤差が互いに補強し合うか、相殺するかが変わる可能性があります。最終的な結果として、低精度の計算では運が良くなり、エラーの方向が異なるため、であるため、低精度の計算が高精度の計算よりも「真の」結果に近い場合があります。

より高い精度で計算を行うと、常に真の答えに近い答えが得られると予想されますが、この引数はそれ以外の場合に表示されます。これは、浮動小数点での計算が「正しい」答えを返すことがありますが、倍精度での計算が「間違った」答えを与えるのはなぜですか?

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

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

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

OK、私はすべての浮動小数点方式が表現エラーのために不正確さを導入すること、そしてそれらの不正確さが時々計算に使用される精度のビット数に基づいて互いに累積または相殺されることを受け入れる。これらの不正確さが一貫性があるであるという保証は少なくともありますか?

いいえ、floatやdoubleについてはそのような保証はありません。コンパイラとランタイムはどちらも、仕様で要求されているよりも高い精度で浮動小数点計算を実行できます。特に、コンパイラーとランタイムは、単精度(32ビット)算術演算64ビット、80ビット、128ビット、または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を生成します。1つのマシンでtrueを生成し、別のマシンでfalseを生成します。同じプログラムで式が2回出現した場合、trueとfalseの両方を生成します。この式は、理由を問わず、どちらの値でもかまいません。コンパイラはここで完全に信頼できないことが許されています。

正しい。

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

これは狂気です。

正しい。

この混乱の責任は誰にあるのですか?

私ではなく、それは確かです。

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

どのようにして一貫した結果を確保できますか?

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

私は倍精度浮動小数点数または浮動小数点数を使用する必要があります。一貫した結果を奨励するために何でもを行うことができますか?

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

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

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

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

131
Eric Lippert

あなたが書くとき

_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での10進数、浮動小数点、および倍精度の違い? および 浮動小数点エラーの扱い) .NETの場合 )ですが、クールな記事を読むことをお勧めします。

_What Every Computer Scientist Should Know About Floating-Point Arithmetic_

まあ、どのようなleppie said がより論理的です。実際の状況はここにあり、完全に依存しますcompiler/computerまたはcpu

レッピーコードに基づいて、このコードはmyVisual Studio 2010およびLinqpadで機能し、結果としてTrue/Falseですが、 ideone.com で試したところ、結果はTrue/Trueになります

[〜#〜] demo [〜#〜]を確認してください。

ヒントConsole.WriteLine(.1f + .2f == .3f);を書いたとき、Resharperが警告を出します。

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

enter image description here

8
Soner Gönül

コメントで述べたように、これはコンパイラが一定の伝播を行い、計算をより高い精度で実行するためです(これは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)

@Caramirielはまた.1f+.2f==.3fはILでfalseとして発行されるため、コンパイラはコンパイル時に計算を行いました。

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

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false
5
leppie

テスト合格後の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)));
}
2
Valentin Kuzub

==は、正確な浮動小数点値を比較することです。

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

0
njzk2

理由はわかりませんが、現時点では私の結果とあなたの結果が一部異なります。 3番目と4番目のテストはたまたま問題に反しているため、説明の一部が間違っている可能性があることに注意してください。

using System;

class Test
{
    static void Main()
    {
        float a = .1f + .2f;
        float b = .3f;
        Console.WriteLine(a == b);                 // true
        Console.WriteLine(a.Equals(b));            // true
        Console.WriteLine(.1f + .2f == .3f);       // true
        Console.WriteLine((1f + .2f).Equals(.3f)); //false
        Console.WriteLine(.1d + .2d == .3d);       //false
        Console.WriteLine((1d + .2d).Equals(.3d)); //false
    }
}
0
imba-tjd