38.400000000000006 は計算ミス?C#の小数計算で起きる丸め誤差を見てみよう

広告

C#でコンソールアプリを作って、小数を使った計算をしたときに、次のような結果が表示されることがあります。

38.400000000000006

本当は、

48 × 0.80 = 38.4

のはずです。

では、なぜ C# では 38.400000000000006 のような長い小数が表示されることがあるのでしょうか。

これは、計算ミスではありません。

小数をコンピュータで扱うときに起きることがある、丸め誤差です。


今回のケース

今回のプログラムでは、ファンクションポイントを計算しました。

入力例は次のとおりです。

EIEOILFEIFEQ補正係数
231120.80

重み付け合計は、次のように計算されます。

2×4 + 3×5 + 1×10 + 1×7 + 2×4
= 8 + 15 + 10 + 7 + 8
= 48

補正係数は 0.80 なので、

48 × 0.80 = 38.4

となるはずです。

しかし、C#で実行すると、環境や表示方法によっては、次のように表示されることがあります。

38.400000000000006

関係するプログラム

今回のプログラムでは、補正係数を double 型で受け取っています。

Console.Write("補正係数:");
double coef = Convert.ToDouble(Console.ReadLine());

double fp = total * coef;

Console.WriteLine($"ファンクションポイント:{fp}");

ここで使っている double は、小数を扱うための型です。

例えば、

double coef = 0.80;

のように、小数を入れることができます。

ただし、ここで大事なのは、double は小数をコンピュータ内部では2進数として保存しているということです。


double は何ビット?

C#の double は 64ビットです。

内訳は次のようになっています。

部分ビット数役割
符号部1ビットプラスかマイナスか
指数部11ビット小数点の位置を表す
仮数部52ビット数字の細かい部分を表す

ただし、仮数部は先頭の 1 を省略して扱う仕組みがあるため、実際には約 53ビット分の精度があります。

ここでは難しい仕組みをすべて覚える必要はありません。

まずは、

double は64ビットの箱で小数を保存している。でも、無限に続く小数を全部入れることはできない。

と考えれば十分です。


0.80を2進数に変換してみる

人間は普段、10進数を使っています。

しかし、コンピュータの中では、数値は基本的に2進数で扱われます。

では、今回の 0.80 を2進数にするとどうなるでしょうか。

小数を2進数に変換するときは、次の方法を使います。

2倍して、整数部分を順番に取り出す

実際にやってみます。

回数計算整数部分残り
10.8 × 2 = 1.610.6
20.6 × 2 = 1.210.2
30.2 × 2 = 0.400.4
40.4 × 2 = 0.800.8
50.8 × 2 = 1.610.6
60.6 × 2 = 1.210.2
70.2 × 2 = 0.400.4
80.4 × 2 = 0.800.8

整数部分を上から順番に並べると、

0.80₁₀
= 0.11001100110011001100...₂

となります。

途中から、

1100

がずっと繰り返されています。

つまり、10進数では 0.80 と短く書ける数が、2進数では途中で終わらないのです。


10進数では簡単でも、2進数では終わらないことがある

例えば、10進数では、

1 ÷ 3 = 0.333333333...

となり、途中で終わりません。

これと同じように、2進数では

0.80₁₀
= 0.11001100110011001100...₂

となり、途中で終わりません。

コンピュータは無限に続く小数を最後まで保存できないため、途中で近い値に丸めて保存します。


C#の中の0.80は少しだけ大きい

C# の double に 0.80 を保存すると、内部では実際には

0.8000000000000000444089209850062616169452667236328125

に近い値になります。

つまり、

0.80

ではなく、

0.80より少しだけ大きい値

が保存されています。


その値に48を掛ける

人間は、

48 × 0.80
= 38.4

と計算します。

しかし、C# の中では実際には、

48 × 0.8000000000000000444089209850062616169452667236328125

を計算しています。

その結果、

38.400000000000002131628207280300557613372802734375

という値になります。

この時点で、すでに本来の 38.4 より少しだけ大きくなっています。


計算結果もdoubleに入る値に丸められる

しかし、この値も double に保存しなければなりません。

ところが、

38.400000000000002131628207280300557613372802734375

も、double で完全には保存できません。

そのため、この値に最も近い double の値へ再び丸められます。

その結果、保存される値は

38.400000000000005684341886080801486968994140625

となり、画面には

38.400000000000006

と表示されることがあります。


つまり、何が起きているのか

0.80
↓
2進数では無限に続く
↓
double(64ビット)には全部保存できない
↓
近い値に丸めて保存
↓
48を掛ける
↓
結果もdoubleに保存できる近い値へ丸める
↓
38.400000000000006
と表示されることがある

これは大きな間違いなのか

いいえ。

これは非常に小さな誤差です。

人間が見ると、

38.400000000000006

は、

38.4

と同じと考えて問題ありません。


F2で表示を整える

画面表示だけを見やすくするには、

Console.WriteLine($"ファンクションポイント:{fp:F2}");

と書きます。

すると、

ファンクションポイント:38.40

と表示されます。

F2 は計算結果を変えるものではありません。

画面に表示するときの見た目を整えているだけです。


修正版プログラム

Console.WriteLine();
Console.WriteLine($"重み付け合計:{total}");
Console.WriteLine($"ファンクションポイント:{fp:F2}");

確認してみよう

EIEOILFEIFEQ補正係数重み付け合計表示結果
231120.804838.40
524130.658957.85
011021.002323.00

まとめ

  • double は64ビットで小数を保存する。
  • 0.80 は2進数では 0.1100110011001100…₂ と無限に続く。
  • double には無限に保存できないため、近い値へ丸められる。
  • その近い値で計算し、計算結果も再び double に保存できる近い値へ丸められる。
  • そのため、38.400000000000006 のような表示になることがある。
  • F2 は計算を直すものではなく、表示を整えるための書式指定である。

ここまでは double を使って計算してきました。

double は高速に計算できる反面、小数を2進数で保存するため、丸め誤差が発生することがあります。

例えば、

38.400000000000006

のような表示になることがあるのは、そのためです。

では、このような場合はどうすればよいのでしょうか。

C#には、もう一つ小数を扱う型として decimal が用意されています。


decimal は10進数で小数を保存する

decimal は、お金や税率、割合など、10進数をできるだけ正確に扱いたいときのために作られた型です。

そのため、

0.80

のような10進数は、そのまま正確に保存できます。


プログラムを書き換えてみよう

double を使っていた部分を、次のように変更します。

decimal coef = 0.80m;
decimal fp = total * coef;

Console.WriteLine(fp);

ここで注意したいのは、

0.80m

の 最後の m です。

この m は、

「この値は decimal 型ですよ」

という意味です。

もし m を付け忘れると、

decimal coef = 0.80;

となり、コンパイルエラーになります。


実行結果を比べてみよう

double を使った場合は、

38.400000000000006

と表示されることがあります。

一方、decimal を使うと、

38.4

と表示されます。

つまり、今回のような計算では、decimal の方が人間の期待どおりの結果になります。


では、いつも decimal を使えばいいの?

実は、そうとも限りません。

double と decimal は、それぞれ得意な分野があります。

得意な用途
double科学技術計算、物理計算、3Dゲーム、シミュレーション
decimal金額、税率、会計、割合など10進数を正確に扱いたい計算

例えば、Unityでゲームを作るときの座標や速度は、ほとんど double や float が使われます。

一方で、銀行システムや会計ソフトでは、decimal がよく使われます。


今回はなぜ double を使って説明したの?

この記事では、あえて double を使いました。

その理由は、

  • コンピュータは2進数で計算すること
  • double は64ビットで数値を保存すること
  • 丸め誤差が発生する理由

を理解してもらうためです。

もし最初から decimal を使ってしまうと、

38.4

と正しく表示されるため、

「なぜ丸め誤差が起きるのか」

を学ぶ機会がなくなってしまいます。


まとめ

今回のようなファンクションポイント計算では、実務であれば decimal を選ぶ方が自然です。

一方で、コンピュータが小数をどのように扱っているのかを学ぶ教材としては、double を使うことで丸め誤差の仕組みを理解できます。

つまり、

  • 仕組みを学ぶなら double
  • 実務で10進数を正確に扱うなら decimal

というように、目的に応じて使い分けることが大切です。

訪問数 1 回, 今日の訪問数 1回

広告

C#

Posted by hidepon