DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

小数(浮動小数点数型)の計算が思った結果にならない理由と解決法
Decimal型はいつ使うか?

小数の計算をしていて、その計算結果が常識では考えられない、変な値になったという経験はないでしょうか?単精度浮動小数点型であるSingle型(C#では、float型)や、倍精度浮動小数点数型であるDouble型(C#では、double型)を使った計算ではそのようなことがあります。ここではそのようなことが起こる理由と、その対策を説明します。

また、Decimal型はどのような時に使うのかについても説明しています。

不可解な小数の計算の例

「0.1 + 0.2 = 0.3」は正しいでしょうか?数学では、当然そうでなくては困ります。

それでは、VB.NETで「0.1 + 0.2 = 0.3」(C#では、「0.1 + 0.2 == 0.3」)は「True」になるでしょうか?実は「False」になります。信じられない方は、実際に以下のようなコードで試してみてください。

VB.NET
コードを隠すコードを選択
Console.WriteLine(0.1 + 0.2 = 0.3)
'False
C#
コードを隠すコードを選択
Console.WriteLine(0.1 + 0.2 == 0.3);
//False

もう一つ例を紹介します。今度は2000円の商品の消費税(5%)がいくらかを計算します。1円以下は繰上げます。

VB.NET
コードを隠すコードを選択
Dim price As Integer = 2000
Dim rate As Single = 0.05F
Console.WriteLine(Math.Ceiling(price * rate))
'101
C#
コードを隠すコードを選択
int price = 2000;
float rate = 0.05f;
Console.WriteLine(Math.Ceiling(price * rate));
//101

このコードを実行してみると、結果は「100」にはならず、「101」になります。なぜこんなことが起こるのでしょうか?

不可解な計算結果の理由

このような数学のルールを無視した計算結果を返すなんてバグに違いないと思われるかもしれませんが、そうではありません。これにはちゃんとした理由があります。

SingleやDoubleのような浮動小数点数型は、値を2進数で格納しています。しかし、ほとんどの10進数の小数は2進数で表現することができません。そのためこのような値はSingleやDouble型では近似値でしか表現することができず、その誤差が上記のような非常識的な計算結果として現れます。

例えば十進数の「0.1」を2進数に変換すると「0.0001100110011…」となり、「0011」の部分が永遠に循環します。よって「0.1」をSingleやDouble型に格納するには、適当な桁で丸める必要があります。丸め方は、最近接偶数への丸めです。結果として「0.1」はDouble型では2進数で「0.0001100110011001100110011001100110011001100110011001101」となります。これを10進数に戻すと「0.1000000000000000055511151231257827021181583404541015625」となり、「0.1」ではなくなります。

ただし整数は、有効桁数(Singleは7桁、Doubleは15桁)の範囲内であれば、正確に格納できます。また小数であっても、k/(2^n)(kとnは整数)で表すこともできる小数は2進数で表現できるため、正確に格納できます。

なおこのような誤差は、.NET Frameworkだけで起こるわけではありません。IEEE 754の2進浮動小数点形式を採用して計算していれば、同じことが起こります。

この誤差についてもっとよく知りたい方は、「日経PC21 / エクセル(Excel)「演算誤差」対策講座」が参考になるかと思います。

補足:DoubleのToStringメソッドをパラメータなしに呼び出した場合は、Doubleの有効桁数である15桁までしか文字列にせず、それ以降は丸められるため、この方法で誤差を確認することはできません。しかしToStringメソッドのパタメータに"G17"を指定すれば、17桁まで知ることができます。例えば、「0.3」と「0.1 + 0.2」をToString()で文字列にするとどちらも「0.3」となりますが、ToString("G17")だと「0.3」は「0.29999999999999999」、「0.1 + 0.2」は「0.30000000000000004」となり、違いが出ます。
VB.NET
コードを隠すコードを選択
Dim d1 As Double = 0.3
Dim d2 As Double = 0.1 + 0.2

Console.WriteLine(d1.ToString()) '0.3
Console.WriteLine(d2.ToString()) '0.3
Console.WriteLine(d1.ToString("G17")) '0.29999999999999999
Console.WriteLine(d2.ToString("G17")) '0.30000000000000004
Console.WriteLine(d1 = d2) 'False
C#
コードを隠すコードを選択
double d1 = 0.3;
double d2 = 0.1 + 0.2;

Console.WriteLine(d1.ToString()); //0.3
Console.WriteLine(d2.ToString()); //0.3

Console.WriteLine(d1.ToString("G17")); //0.29999999999999999
Console.WriteLine(d2.ToString("G17")); //0.30000000000000004

Console.WriteLine(d1 == d2); //False

変数に代入するかしないか、最適化するかしないかによって計算結果が異なる

今まで説明してきた事柄は一般的によく知られています。しかしこれ以外にも浮動小数点数の計算を狂わせる要因があります。

例えば具体例の2番目に紹介したコードですが、計算結果を変数に代入してからMath.Ceilingメソッドで切り上げると、違う結果になります。また、消費税を直接計算した値と、計算結果を変数に代入した値を等号演算子で比較すると、「False」になってしまいます。

VB.NET
コードを隠すコードを選択
Dim price As Integer = 2000
Dim rate As Single = 0.05F
Dim tax As Single = price * rate
Console.WriteLine(Math.Ceiling(tax)) '100
Console.WriteLine((price * rate) = tax) 'False
C#
コードを隠すコードを選択
int price = 2000;
float rate = 0.05f;
float tax = price * rate;
Console.WriteLine(Math.Ceiling(tax)); //100
Console.WriteLine((price * rate) == tax); //False

この理由を一言で言うと、直接計算した値の方が、変数に代入した値よりも精度が高いためです。

ECMA-335「Partition III: CIL Instruction Set」の日本語訳)によると、浮動小数点数は、それと同じか、それ以上の精度、サイズ(桁数、最大値と最小値の範囲)を持った内部表現に変換して使用することができます。内部表現というのは、例えばx87 FPUの80ビット表現のように、通常はハードウェアで効率的に操作できる表現のことです。ただしこれは、クラスのフィールド、配列の要素、静的変数以外の、引数、戻り値、評価スタック、ローカル変数の場合のみです。

上記の例の場合、「price * rate」は精度の高い内部表現のままですが、変数「tax」に代入すると、より精度の低いSingle型に変換されるため、はみ出た桁が丸められます。そのため、両者は違う値として判断されます。

それでは変数に代入すれば必ず内部表現からその型の精度に戻されるかというと、そうとも言い切れません。例えば上記の例で、4行目を削除してから(しなくても大丈夫かもしれません)、「コードの最適化」を有効にして(構成がリリースであれば、通常は有効になっています)、「デバッグなしで開始」(Ctrl + F5)すると、今度は「True」になります。この場合は、taxも内部表現のままの精度です。

補足:もしどうしても内部表現からSingle型の精度に戻したいということであれば、Single型にキャストします。例えば以下のようにキャストしてから比較すれば、最適化の有無にかかわらず常にTrueになります。
VB.NET
コードを隠すコードを選択
Console.WriteLine(CSng(price * rate) = CSng(tax))
C#
コードを隠すコードを選択
Console.WriteLine((float)(price * rate) == (float)tax);

JITの最適化を有効にするかしないかで結果が変わる例が「Binary floating point and .NET」の「Comparing floating point numbers」にもありますので、興味のある方はそちらもご覧ください。

対策

Decimal型を使う

10進数の小数を2進数に変換するときの丸め誤差をなくすには、SingleやDouble型の代わりにDecimal型を使用します。Decimal型は10進数の小数でも正確に表現することができます。

VB.NET
コードを隠すコードを選択
Dim d1 As Decimal = 1.000001D
Dim d2 As Decimal = 0.000001D
Console.WriteLine((d1 - d2) = 1D) 'True

Dim price As Integer = 2000
Dim rate As Decimal = 0.05D
Console.WriteLine(Math.Ceiling(price * rate)) '100
C#
コードを隠すコードを選択
decimal d1 = 1.000001m;
decimal d2 = 0.000001m;
Console.WriteLine((d1 - d2) == 1.0m); //True

int price = 2000;
decimal rate = 0.05m;
Console.WriteLine(Math.Ceiling(price * rate)); //100

ただし、Decimal型で計算すれば必ず正しい計算結果を得られるという訳ではありません。例えば、「1m / 3m * 3m」の結果は「1」にならず、「0.9999999999999999999999999999」になります。

Decimal型の欠点は、パフォーマンスが悪いことと、格納できる値の大きさの範囲が狭いことです。よって通常は、Decimalを使用するのは、精度の高い計算が要求されるときだけです。例えば、誤差の許されないお金の計算(財務計算)にはDecimalを使用します。一方、はじめに与えられた値から誤差があるような科学計算では、Decimalを使用する意味がありません。

補足:Double型と比べてSingle型のメリットはほとんどありませんので、Single型を積極的に使用することはまずありません。
補足:Decimal型も浮動小数点数(floating point number)です。固定小数点数(fixed point number)ではありません。SingleやDouble型が2進浮動小数点数(binary floating point number)なのに対して、Decimal型は10進浮動小数点数(decimal floating point number)と呼ばれます。

許容範囲を決めて値を比較する

今まで見てきたように、2進浮動小数点数の比較は非常に厄介です。例えば、2つの2進浮動小数点数が等しいかを判断するのに = (C#では、 == )を使うのは危険です。両者が全く同じ値の時だけ等しいと判断するのではなく、「両者の差の絶対値がこの値以内ならば等しいと判断する」という許容範囲を決めて比較するのが安全です。

例えば次の例では、許容範囲を 0.000001 として2つのDouble値を比較しています。

VB.NET
コードを隠すコードを選択
Dim d1 As Double = 0.1 + 0.2
Dim d2 As Double = 0.3
If Math.Abs(d1 - d2) < 0.000001 Then
    Console.WriteLine("等しいと判断する")
End If
C#
コードを隠すコードを選択
double d1 = 0.1 + 0.2;
double d2 = 0.3;
if (Math.Abs(d1 - d2) < 0.000001)
{
    Console.WriteLine("等しいと判断する");
}
補足:許容範囲は、0より大きい最小の値を示すDouble.Epsilonフィールドよりも大きい値でないと意味がありません。

また、具体例の2番目に示したように小数を切り上げる場合は、切り上げる値からあらかじめ許容できる範囲の小さな値を引いておくという方法があります。

VB.NET
コードを隠すコードを選択
Dim price As Integer = 2000
Dim rate As Single = 0.05F
Console.WriteLine(Math.Ceiling(price * rate - 0.0001F))
C#
コードを隠すコードを選択
int price = 2000;
float rate = 0.05f;
Console.WriteLine(Math.Ceiling(price * rate - 0.0001f));

整数化する

上記以外に考えられる対策としては、小数を整数にして計算するという方法があります。例えば「0.1 + 0.2 = 0.3」であれば、両辺を10倍して「1 + 2 = 3」とすれば正しい結果を得られます。ただし小数をそのまま10倍しただけではきっちり整数になっていない可能性もありますので、Math.Roundメソッドを使って整数に丸めます。

ここに示した方法以外にも有用な対策があるかもしれません。お気付きの方がいらっしゃいましたら、ぜひ教えてください。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。