【小ネタ】longをfloatにキャストしたときの丸め誤差について

2022/07/20に公開

はじめに

long型の値をfloatやdoubleにキャストした際に発生する誤差(丸め誤差)について軽く書いてみようと思います。

※ 間違っているところがあればコメントにてご指摘いただけると幸いです。

環境

Unity 2021.3.5f1

実験 : longをfloatへキャストし、longに戻す

7777777777777777777 というlong値をfloatにキャストした後、longに戻すだけの簡単なサンプルです。
7777777777777777777 は 19桁の整数です。

public class FloatRoundingError : MonoBehaviour
{
    void Start()
    {
        long x = 7777777777777777777;
        float f = (float)x;
        long y = (long)f;
        
        Debug.Log("x = " + x);
        Debug.Log("f = " + f);
        Debug.Log("y = " + y);

        Debug.Log("誤差(x - y) = " + (new decimal(x) - new decimal(y)));
        Debug.Log("誤差(y / x) = " + (new decimal(y) / new decimal(x)));        

    }
}

実行結果

以下のようなログが出力されます。
誤差(x と yの差)は 約2000億(198413589617)となっています。

x = 7777777777777777777
f = 7.777778E+18
y = 7777777579364188160
誤差(x - y) = 198413589617
誤差(y / x) = 0.999999974489681

2ビットで表現

x, y, f を 2進数で表示します。

x = 0110101111110000001101111010111000110010010111110001110001110001
f = 01011110110101111110000001101111
y = 0110101111110000001101111000000000000000000000000000000000000000

xとyのビット値を比べてみます。

xの24ビット分の情報は保持されていますが、それ以降はすべて0になっていることが分かります。
丸め誤差が発生していることが分かります。

longからfloatへのキャスト(丸め誤差)

7777777777777777777 という整数は、浮動小数点数で表現すると、
7.777777777777777777 * 10^18 となります。

浮動小数点数において、7.777777777777777777 を仮数、10^18 を指数と呼びます。

Unity C#におけるfloatは、
最上位1ビットを符号、上位8ビットを指数、下位23ビットを仮数を表現するために使用します。

誤差の桁数

仮数部の23ビット では、有効数字6~7桁程度の10進数を表現できます。
(log_{10} 2^{23} \approx 6.923)

7777777777777777777 は 19桁の数で、これをfloatにキャストすると12~13桁程度の丸め誤差が出ます。
今回の誤差 198413589617 は12桁の数です。

long x = 7777777777777777777; // 7.777777777777777777 * 10^18
float f = (float)x; // 7.777778 * 10^18 (丸め誤差が発生)
検証コード (longをfloatにキャストし、longに戻す)
using System;
using UnityEngine;

public class RoundingError : MonoBehaviour
{
    void Start()
    {
        long x = 7777777777777777777;
        float f = (float)x;
        long y = (long)f;
        
        Debug.Log("x = " + x);
        Debug.Log("f = " + f);
        Debug.Log("y = " + y);
        
        Debug.Log("誤差(x - y) = " + (new decimal(x) - new decimal(y)));
        Debug.Log("誤差(y / x) = " + (new decimal(y) / new decimal(x)));
        
        Debug.Log("x(2bit) = " + Convert.ToString(x, 2));
        Debug.Log("f(2bit) = " + FloatToBinaryString(f));
        Debug.Log("y(2bit) = " + Convert.ToString(y, 2));
    }

    // float を 2bitの文字列に変換
    string FloatToBinaryString(float f)
    {
        int x = BitConverter.ToInt32(BitConverter.GetBytes(f), 0);
        return Convert.ToString(x, 2);
    }
}

実験 : longからdoubleへキャストし、longに戻す

longをdoubleにキャストした場合はどうなるのかも見てみましょう。

long x = 7777777777777777777;
double d = x;
long y = (long)d;
検証コード (longをdoubleにキャストし、longに戻す)
using System;
using UnityEngine;

public class DoubleRoundingError : MonoBehaviour
{
    void Start()
    {
        long x = 7777777777777777777;
        double d = x;
        long y = (long)d;
        
        Debug.Log("x = " + x);
        Debug.Log("d = " + d);
        Debug.Log("y = " + y);

        Debug.Log("誤差(x - y) = " + (new decimal(x) - new decimal(y)));
        Debug.Log("誤差(y / x) = " + (new decimal(y) / new decimal(x)));        

        Debug.Log("x(2bit) = " + LongToBinaryString(x));
        Debug.Log("d(2bit) = " + DoubleToBinaryString(d));
        Debug.Log("y(2bit) = " + LongToBinaryString(y));
    }

    // long を 2bitに変換
    string LongToBinaryString(long x)
    {
        string s = "";
        for (int i = 0; i < 64; i++)
        {
            s = (x & 1) + s;
            x = x >> 1;
        }
        return s;
    }
    
    // double を 2bitに変換
    string DoubleToBinaryString(double d)
    {
        long x = BitConverter.ToInt64(BitConverter.GetBytes(d), 0);
        return LongToBinaryString(x);
    }
}

floatでは2000億の誤差が出ていましたが、doubleでは誤差113です。

x = 7777777777777777777
d = 7.77777777777778E+18
y = 7777777777777777664
誤差(x - y) = 113
誤差(y / x) = 0.9999999999999999854714285714

これらの数をビット数として表すと、以下のようになります。

x = 0110101111110000001101111010111000110010010111110001110001110001
d = 0100001111011010111111000000110111101011100011001001011111000111
y = 0110101111110000001101111010111000110010010111110001110000000000

x と y のビットを並べてみました。

xの下位ビット列 1110001 が yでは 0000000になっています。(丸め誤差 113)

誤差の桁数

double は倍精度浮動小数点数と呼ばれ、その仮数部のサイズは52ビットです。
仮数部は有効数字15桁 ~ 16桁程度の浮動小数点数を表現することができます。
(log_{10} 2^{52} \approx 15.65)

7777777777777777777 は 19桁の数で、これをdoubleにキャストすると、2~3桁程度の丸め誤差が出ます。
今回の誤差 113 は、3桁の数です。

まとめ

19桁のlong値 を float にキャストすると、最大で12桁 ~ 13桁の丸め誤差が出る (数千億くらいの誤差)
19桁のlong値 を double にキャストすると、最大で2桁 ~ 3桁の丸め誤差が出る (数百くらいの誤差)

Discussion