web-dev-qa-db-ja.com

C#で10進数を2進数に変換すると違いが生じます

問題の要約:

一部の10進数値では、型を10進数から2進数に変換すると、結果に小さな端数が追加されます。

さらに悪いことに、変換時に異なるdouble値になる2つの「等しい」小数値が存在する可能性があるということです。

コードサンプル:

decimal dcm = 8224055000.0000000000m;  // dcm = 8224055000
double dbl = Convert.ToDouble(dcm);    // dbl = 8224055000.000001

decimal dcm2 = Convert.ToDecimal(dbl); // dcm2 = 8224055000
double dbl2 = Convert.ToDouble(dcm2);  // dbl2 = 8224055000.0

decimal deltaDcm = dcm2 - dcm;         // deltaDcm = 0
double deltaDbl = dbl2 - dbl;          // deltaDbl = -0.00000095367431640625

コメントの結果を見てください。結果は、デバッガーのウォッチからコピーされます。この効果を生む数値は、データ型の制限よりもはるかに少ない10進数であるため、オーバーフローすることはできません(推測です!)。

さらに興味深いのは、2つの 等しい 10進値(上記のコードサンプルでは、​​「deltaDcm」がゼロに等しい「dcm」および「dcm2」を参照) 違う 変換されたときの二重値。 (コードでは、ゼロ以外の「deltaDbl」を持つ「dbl」および「dbl2」)

私はそれが2つのデータタイプの数値のビットごとの表現の違いに関連するものであるべきだと思いますが、何を理解することはできません!そして、変換を必要な方法にするために何をすべきかを知る必要があります。 (dcm2-> dbl2など)

43
Iravanchi

答えは、decimalが有効桁数を保持しようとするという事実にあります。したがって、8224055000.0000000000mには有効数字が20桁あり、82240550000000000000E-10として格納されますが、8224055000mには10個しかなく、8224055000E+0として格納されます。 doubleの仮数は(論理的に)53ビット、つまり最大16桁の10進数です。これは正確にdoubleに変換したときに得られる精度であり、実際の例の浮遊1は小数点第16位です。 doubleは基数2を使用するため、変換は1対1ではありません。

数値のバイナリ表現は次のとおりです。

dcm:
00000000000010100000000000000000 00000000000000000000000000000100
01110101010100010010000001111110 11110010110000000110000000000000
dbl:
0.10000011111.1110101000110001000111101101100000000000000000000001
dcm2:
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000001 11101010001100010001111011011000
dbl2 (8224055000.0):
0.10000011111.1110101000110001000111101101100000000000000000000000

Doubleの場合、符号、指数、仮数フィールドを区切るためにドットを使用しました。 10進数については、 decimal.GetBitsのMSDN を参照してください。ただし、本質的に最後の96ビットは仮数です。 dcm2の仮数ビットとdbl2の最上位ビットが正確に一致することに注意してください(doubleの仮数の暗黙的な1ビットを忘れないでください)。実際、これらのビットは8224055000を表します。dblの仮数ビットはdcm2およびdbl2と同じですが、最下位ビットの厄介な1を表します。 dcmの指数は10、仮数は82240550000000000000です。

Update II:実際には、末尾のゼロを切り落とすのは非常に簡単です。

// There are 28 trailing zeros in this constant —
// no decimal can have more than 28 trailing zeros
const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000m ;

// decimal.ToString() faithfully prints trailing zeroes
Assert ((8224055000.000000000m).ToString () == "8224055000.000000000") ;

// Let System.Decimal.Divide() do all the work
Assert ((8224055000.000000000m / PreciseOne).ToString () == "8224055000") ;
Assert ((8224055000.000010000m / PreciseOne).ToString () == "8224055000.00001") ;
25
Anton Tykhyy

記事 すべてのコンピューター科学者が浮動小数点演算について知っておくべきこと は、始めるのに最適な場所です。

簡単な答えは、浮動小数点2進算術演算は必ず近似であり、常に推測される近似値ではないということです。これは、CPUが基数2で演算を行うのに対し、人間は(通常)基数10で演算を行うためです。これに起因するさまざまな予期しない効果があります。

5
Greg Hewgill

この問題をよりわかりやすく説明するために、LinqPadでこれを試してください(または、すべての.Dump()を置き換えて、お望みであればConsole.WriteLine()sに変更してください)。

論理的には、小数の精度が3つの異なるdoubleになる可能性があると思われます。/PreciseOneのアイデアに対する@AntonTykhyyへの称賛:

((double)200M).ToString("R").Dump(); // 200
((double)200.0M).ToString("R").Dump(); // 200
((double)200.00M).ToString("R").Dump(); // 200
((double)200.000M).ToString("R").Dump(); // 200
((double)200.0000M).ToString("R").Dump(); // 200
((double)200.00000M).ToString("R").Dump(); // 200
((double)200.000000M).ToString("R").Dump(); // 200
((double)200.0000000M).ToString("R").Dump(); // 200
((double)200.00000000M).ToString("R").Dump(); // 200
((double)200.000000000M).ToString("R").Dump(); // 200
((double)200.0000000000M).ToString("R").Dump(); // 200
((double)200.00000000000M).ToString("R").Dump(); // 200
((double)200.000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.0000000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000000M).ToString("R").Dump(); // 200.00000000000003
((double)200.000000000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.00000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997

"\nFixed\n".Dump();

const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000M;
((double)(200M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
2
Ilan

これは古い問題であり、StackOverflowに関する多くの同様の質問の主題となっています。

simplisticの説明は、10進数を正確に2進数で表現できないことです。

このリンク は問題を説明するかもしれない記事です。

1
pavium