38.400000000000006 は計算ミス?C#の小数計算で起きる丸め誤差を見てみよう
C#でコンソールアプリを作って、小数を使った計算をしたときに、次のような結果が表示されることがあります。
38.400000000000006
本当は、
48 × 0.80 = 38.4
のはずです。
では、なぜ C# では 38.400000000000006 のような長い小数が表示されることがあるのでしょうか。
これは、計算ミスではありません。
小数をコンピュータで扱うときに起きることがある、丸め誤差です。
今回のケース
今回のプログラムでは、ファンクションポイントを計算しました。
入力例は次のとおりです。
| EI | EO | ILF | EIF | EQ | 補正係数 |
|---|---|---|---|---|---|
| 2 | 3 | 1 | 1 | 2 | 0.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倍して、整数部分を順番に取り出す
実際にやってみます。
| 回数 | 計算 | 整数部分 | 残り |
|---|---|---|---|
| 1 | 0.8 × 2 = 1.6 | 1 | 0.6 |
| 2 | 0.6 × 2 = 1.2 | 1 | 0.2 |
| 3 | 0.2 × 2 = 0.4 | 0 | 0.4 |
| 4 | 0.4 × 2 = 0.8 | 0 | 0.8 |
| 5 | 0.8 × 2 = 1.6 | 1 | 0.6 |
| 6 | 0.6 × 2 = 1.2 | 1 | 0.2 |
| 7 | 0.2 × 2 = 0.4 | 0 | 0.4 |
| 8 | 0.4 × 2 = 0.8 | 0 | 0.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}");
確認してみよう
| EI | EO | ILF | EIF | EQ | 補正係数 | 重み付け合計 | 表示結果 |
|---|---|---|---|---|---|---|---|
| 2 | 3 | 1 | 1 | 2 | 0.80 | 48 | 38.40 |
| 5 | 2 | 4 | 1 | 3 | 0.65 | 89 | 57.85 |
| 0 | 1 | 1 | 0 | 2 | 1.00 | 23 | 23.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
というように、目的に応じて使い分けることが大切です。






ディスカッション
コメント一覧
まだ、コメントがありません