より良いエンジニアを目指して

1日1つ。良くなる!上手くなる!

浮動小数点数の比較は誤差が出る。どうすれば?

浮動小数点数の比較は誤差が出ます。

    class Program
    {
        static void Main(string[] args)
        {
            double a = 0;
            for (int i = 0; i < 10; i++)
            {
                a += 0.1d;
            }

            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine($"{a} == 1.0d");
            if (a == 1.0d)
            {
                Console.WriteLine( "Equal");
            }
            else
            {
                Console.WriteLine("Not Equal");
            }
        }
    }

f:id:rimever:20200422191946p:plain
1.0ではなく、0.99999...となる

対策としては、いくつか考えられます。

decimal型にする

お金の計算などではこちらでしょう。

using System;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            decimal a = 0;
            for (int i = 0; i < 10; i++)
            {
                a += 0.1m;
            }

            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine($"{a} == 1.0");
            if (a == 1.0m)
            {
                Console.WriteLine( "Equal");
            }
            else
            {
                Console.WriteLine("Not Equal");
            }
        }
    }
}

f:id:rimever:20200422192410p:plain

差分で比較する

同僚からお金の計算をfloatで行ってたから、あとで重大な手戻りが発生したというのは聞いたことはありますが、

自分の経験としては、

小数点を使うぞ、じゃあdecimalを使おう

という経験はありません。

decimalの欠点としては、パフォーマンスが悪いという点です。

とはいえ、個人的にはパフォーマンスより安全性を取るので、これから小数点は全てdecimalで宣言しても良い気にもなります。

しかし、既存コードの兼ね合いで、floatで宣言されているから変えようない状態で比較が必要になるというケースが多いです。

floatのまま、比較を行うことになります。

f:id:rimever:20200422192042p:plain
Resharperでも提案されます。

Resharperの提案にしたがって変換をかけると以下のようなコードに変換されます。

if (Math.Abs(a - 1.0d) < TOLERANCE)

TOLERANCEの部分には、自分で許容誤差を指定することになります。

個人的にはいろいろ考えたくないし、あちこちで宣言することを考えると、マジックコードであれば、宣言する度にバラツキが出てしまいます。

なので、

if (Math.Abs(a - 1.0d) < double.Epsilon)

としていました。標準ライブラリの定数だからこれならどこでも使えるし良いじゃんと思ってました。

しかし、

docs.microsoft.com

Double.Epsilon は、等しいかどうかをテストするときに2つの Double 値の間の距離の絶対測定値として使用されることがあります。 ただし Double.Epsilon は、値がゼロである Double に加算または減算できる最小値を測定します。 正および負の Double 値の場合、Double.Epsilon の値が小さすぎて検出できません。 したがって、値がゼロの場合を除いて、等しいかどうかのテストでは使用しないことをお勧めします。

というわけで、これは避けた方が良いようです。

f:id:rimever:20200422193509p:plain
あ、本当だ。

じゃあ、どうすれば? と考えたり調べたりしたものの、答えは特に書いてませんでした。

結果的には、入力の可能性のある値で判定するしかないというところでしょうか。

例えば、小数点3桁まで入力ができるならば、0.001が入力できるから0.0001か。などと。

でも、それも想定外なケースがありそうでなんか不安。 0.001 * 0.001とか使われそうだし。

Double 値の有効桁数は最大15桁ですが、内部的には最大17桁が保持されます。

とあるので、0.000000000000001dあたりが無難なんですかね。

これは難しいです。

ややこしいことが起きるようであれば、decimalを使う方が良さそう。