質問は紛らわしい表現です。それを多くの小さな質問に分解してみましょう。
浮動小数点演算で、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 の実装には、コンパイラのすべての新しいバージョンがこの動作をすることを確認する回帰テストがあります。
この件に関して言語仕様が述べなければならないのは、実装の裁量で浮動小数点演算をより高い精度で実行できるということだけです。