【知らないと危ない】浮動小数点の「丸め誤差」とは 原因と対策法を解説

アイキャッチ画像

導入

JavaやPythonなどで小数の計算をしていると、時々計算結果がおかしくなることに気付いた人がいるかもしれません。
え?そんなことないけど、という人は以下の実行結果を見てください。

System.out.println(0.1 + 0.2 == 0.3);
// 出力結果
// false

0.1+0.2と0.3はイコールではない?
そんなバカな、と感じるでしょうがこれがまさに「丸め誤差」です。

丸め誤差とは

丸め誤差とは、「ある値を表現できる範囲に収めるために生じる、わずかな誤差」のことです。
コンピュータは有限のメモリ空間を使って数値を扱っており、無限に続く小数を完全に再現することはできません。
そのため、どうしても近似的な数値で計算することになり、結果として誤差が生まれます。
その一例が上で示した結果です。

検証

0.1+0.2=0.3となるはずが、丸め誤差のせいでずれているのを実際に内部の値を確認してみましょう。


・Javaで0.1+0.2の中身確認

double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println("0.1 + 0.2 = " + c);
// 出力結果
// 0.1 + 0.2 = 0.30000000000000004

たしかに、ぴったり0.3ではなく途中で4という数字が現れています。
このせいで、「0.1+0.2==0.3」の結果がfalseになっていたのです。

丸め誤差の原因

浮動小数点数は、2進数で表現された指数表記で格納されています。
例えば、10進数の「0.1」は2進数では無限小数になってしまい、コンピュータは途中で打ち切って記録します。
コード上で「0.1」と表示されている数値は、厳密には「0.1に限りなく近い別の数値」なのです。
プログラミング言語はもちろん、コンピュータ自体が2進数を基に作られているため、これを完全に解決するのは現実的ではありません。
(補足:Javaの場合、float型よりもdouble型の方が扱えるビット数が多いため、誤差は少なくなります。上記コード例はすべてdouble型です。)

対処法

丸め誤差はシステムによっては致命的な問題(金融など)になります。
ここでは代表的な対処法を紹介します。

対処法①:BigDecimalクラス

JavaではBigDecimalクラスを使うことで、高い精度を保った計算が可能です。
最初に文字列として扱うことで、高精度の計算を実現しています。
ただし、BigDecimalでも完全に正しい小数の計算ができるわけではないことには留意してください。

import java.math.BigDecimal;
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal c = a.add(b);
System.out.println(c.doubleValue()==0.3); // BigDecimalからdouble型に変換して比較
// 出力結果
// true

対処法②:あらかじめ誤差を考慮して比較する

根本的な解決にはなりませんが、最初から誤差を考慮するという方法もあります。

// 誤差を考慮したニアリーイコールメソッドを定義
// 2つの小数の差の絶対値が指定値未満であればtrueを返す
public static boolean nearlyEqual(double a, double b, double epsilon) {
    return Math.abs(a - b) < epsilon;}
double d1 = 0.1 + 0.2;double d2 = 0.3;
System.out.println(nearlyEqual(d1, d2, 0.001));
// 出力結果
// true

まとめ

浮動小数点数の丸め誤差は、目に見えにくい落とし穴です。
特に、数字の正確性が求められる金融業界のプロジェクトに携わる場合には必ず誤差を考慮する必要があります。
Javaに限らず多くの言語で同様の現象が起こるため、現象を理解し、適切な手段を使うことが重要です。
特に「なぜか値が合わない」、「テストが通らない」といったケースで、丸め誤差の可能性を疑う習慣を持つことで、バグの早期発見や品質向上につながります。