セージ の メモ書き

メモこそ命の恩人だ

C# - 浮動小数点型(float/double/decimal)

浮動小数点 (floating point number)

表現方式 小数点の位置 利点 欠点
浮動小数 可変 表現できる範囲が広い データ解析に処理時間が必要
固定小数点 固定 ↑と逆 ↑と逆。単純なので高速
  • コンピュータ上で "小数" を表現する方法の一つ。
  • "IEEE 754" で標準化されている。
  • 浮動小数点は、"符号、仮数、指数" で表現する。
    • 符号:正 or 負
    • 指数:2n の n を2進数にした値(データ型ごとにオフセットが異なる)
    • 仮数:正規化した小数点以下の値(1.XXX… に調整し、X の部分を使用)

循環小数による誤差

名称 内容
循環小数 "数値の並びに規則があり" 繰り返される数。
1/7:0.1428571428571429...
1/35:0.0285714285714286...
無限小数 "数値の並びに規則がなく" 無限に続く数。
円周率:3.14...
  • "10進数→2進数" の変換で、循環小数になるケースが多い。
  • 循環小数の場合、途中で切り捨てが生じる。それが誤差になる。

色々試したが、ほとんど循環小数になる。。。

string ConvertNumber(double decimalNumber, int toBase) 
{
    var numberList = new List<int>();
    var nowValue = decimalNumber;
    while (nowValue != 0)
    {
        // 循環小数の対策
        const int maxLength = 20;
        if (numberList.Count > maxLength) break;

        nowValue *= toBase;
        var integer = (int)nowValue;
        numberList.Add(integer);
        if (nowValue >= 1) nowValue -= integer;
    }
    return $"0.{string.Join("", numberList)}";
}

0.1~0.9 の小数を2進数に変換する。0.5以外は循環小数だった。

Debug.WriteLine(ConvertNumber(0.1, toBase: 2));
//0.000110011001100110011
Debug.WriteLine(ConvertNumber(0.2, toBase: 2));
//0.001100110011001100110
Debug.WriteLine(ConvertNumber(0.3, toBase: 2));
//0.010011001100110011001
Debug.WriteLine(ConvertNumber(0.4, toBase: 2));
//0.011001100110011001100
Debug.WriteLine(ConvertNumber(0.5, toBase: 2));
//0.1
Debug.WriteLine(ConvertNumber(0.6, toBase: 2));
//0.100110011001100110011
Debug.WriteLine(ConvertNumber(0.7, toBase: 2));
//0.101100110011001100110
Debug.WriteLine(ConvertNumber(0.8, toBase: 2));
//0.110011001100110011001
Debug.WriteLine(ConvertNumber(0.9, toBase: 2));
//0.111001100110011001100

指数表記(E 表記)

表記 意味
1E+0 1×100 1
1E+1 1×101 10
1E+2 1×102 100
3E+2 3×102 300
  • Exponential:指数
  • E:10 のべき乗を表す。
  • 短い表記で大きい数値を表現できる。


浮動小数点型(float/double/decimal)

docs.microsoft.com

docs.microsoft.com

データ型 範囲 サイズ(byte)
float -3.4028235E+38~3.4028235E+38 4
double -1.7976931348623157E+308~1.7976931348623157E+308 8
decimal -79228162514264337593543950335~79228162514264337593543950335 16
Debug.WriteLine($"{float.MinValue}~{float.MaxValue}");
Debug.WriteLine($"size : {sizeof(float)}, default : {default(float)}");
// -3.4028235E+38~3.4028235E+38
// size : 4, default : 0

Debug.WriteLine($"{double.MinValue}~{double.MaxValue}");
Debug.WriteLine($"size : {sizeof(double)}, default : {default(double)}");
// -1.7976931348623157E+308~1.7976931348623157E+308
// size : 8, default : 0

Debug.WriteLine($"{decimal.MinValue}~{decimal.MaxValue}");
Debug.WriteLine($"size : {sizeof(decimal)}, default : {default(decimal)}");
// -79228162514264337593543950335~79228162514264337593543950335
// size : 16, default : 0

float

Single 構造体 (System) | Microsoft Docs

領域 桁位置 サイズ(bit) 備考
符号部 31 1
指数部 23-30 8 127 を加算
仮数 0-22 23

0.625 (2進数だと 0.101) のデータの並びを確認してみる。

  • 10進数:0.625 => 2進数:0.101
  • 正規化
    • 0.101 × 20
    • 1.01 × 2^-1
  • 各領域の期待値
    • 符号部:0
    • 指数部:-1+127=126 => 111 1110
    • 仮数部:0100 0000 0000 0000 0000 000
  • マージして16進数に変換
    • 0 + 0111 1110 + 0100 0000 0000 0000 0000 000
    • 0011 1111、0010 0000、0000 0000、0000 0000
    • 0x3F 0x20 0x00 0x00
var bytes = BitConverter.GetBytes(0.625f);
bytes.Reverse().ToList().ForEach(x => Debug.Write($"{x:X2} "));
Debug.WriteLine("");
// 3F 20 00 00 
// 0.625 が予想通りの形式で生成されていることを確認。
// 整数型と比較して処理コストが大きそう。

double

Double 構造体 (System) | Microsoft Docs

領域 桁位置 サイズ(bit) 備考
符号部 63 1
指数部 52-62 11 1023 を加算
仮数 0-51 52

前述と同様、0.625 (2進数だと 0.101) のデータの並びを確認してみる。

  • 10進数:0.625 => 2進数:0.101
  • 正規化
    • 0.101 × 20
    • 1.01 × 2^-1
  • 各領域の期待値
    • 符号部:0
    • 指数部:-1+1023=1022 => 011 1111 1110
    • 仮数部:0100 0000 0000 0000 0000 ... 52桁分
  • マージして16進数に変換
    • 0 + 011 1111 1110 + 0100 0000 0000 0000 0000 ... 52桁分
    • 0011 1111、1110 0100、0000 0000、0000 0000、...
    • 0x3F 0xE4 0x00 0x00 0x00 ...
var bytes = BitConverter.GetBytes((double)0.625);
bytes.Reverse().ToList().ForEach(x => Debug.Write($"{x:X2} "));
Debug.WriteLine("");
// 3F E4 00 00 00 00 00 00 
// 0.625 が予想通りの形式で生成されていることを確認。

decimal

Decimal 構造体 (System) | Microsoft Docs

  • decimal は、10進数の小数として扱う。float/double と仕組みが異なる。
  • float/double は、10進数を2進数の小数に変換する。
    • 循環小数となり、切り捨てによって誤差が生じる。
  • decimal は、float/double と比較して...
    • 誤差が小さい。財務計算に向いている。
    • パフォーマンスが悪い。

試しに、0.1 を加算してみる。誤差が生じるか?

// float の場合
void Check(int maxCount) 
{
    float value = 0.0f;
    Enumerable.Range(1, maxCount).ToList().ForEach(x => value += (float)0.1);
    Debug.WriteLine($"sum : {value}");
}

Check(maxCount: 5);
// sum : 0.5
Check(maxCount: 10);
// sum : 1.0000001
Check(maxCount: 50);
// sum : 4.9999976

// 誤差が生じることを確認。
// double の場合
void Check(int maxCount) 
{
    double value = 0.0;
    Enumerable.Range(1, maxCount).ToList().ForEach(x => value += (double)0.1);
    Debug.WriteLine($"sum : {value}");
}

Check(maxCount: 5);
// sum : 0.5
Check(maxCount: 10);
// sum : 0.9999999999999999
Check(maxCount: 50);
// sum : 4.999999999999998

// 誤差が生じることを確認。
// float よりも誤差が小さい。
void Check(int maxCount) 
{
    decimal value1 = 0.0m;
    Enumerable.Range(1, maxCount).ToList().ForEach(x => value1 += (decimal)0.1);
    Debug.WriteLine($"sum : {value1}");
}

Check(maxCount: 5);
// sum: 0.5
Check(maxCount: 10);
// sum: 1.0
Check(maxCount: 50);
// sum: 5.0

Check(maxCount: 1000000);
// sum : 100000.0

// 0.1 の加算処理の場合、誤差が発生しないことを確認。


注意

小数の一致判定

  • 誤差が生じることを考慮した判定が必要。
  • 許容範囲を決めて判定する。
bool IsEqual(double value1, double value2) 
{
    const double threshold = 0.0000001;
    return Math.Abs(value1 - value2) < threshold;
}

double value = 0.0;
Enumerable.Range(1, 10).ToList().ForEach(x => value += (double)0.1);
Debug.WriteLine(value1);
// 0.9999999999999999

Debug.WriteLine(value1 == 1.0);
// False

Debug.WriteLine(IsEqual(value1, 1.0));
// True


実験

キャストの処理時間

  • 内容
    • 浮動小数点にキャストするのは時間がかかるのでは?
    • 以下のキャストの処理時間をチェックする。
      • int→long(整数型から整数型へのキャスト)
      • int→float
      • int→double
      • int→decimal
  • 結果
    • decimal が最も遅い。予想通り。
    • long/float/double は時間がほぼ同じ。float/doubleが意外と速かった。
void Check(Action action)
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    action();
    stopwatch.Stop(); ;
    Debug.WriteLine(stopwatch.Elapsed);
}

// ループ:10億回
var list = Enumerable.Range(0, 1000000000).ToList();

Check(new Action(() =>
{
    list.ForEach(x => { long dummy = x; });
}));

Check(new Action(() =>
{
    list.ForEach(x => { float dummy = x; });
}));

Check(new Action(() =>
{
    list.ForEach(x => { double dummy = x; });
}));

Check(new Action(() =>
{
    list.ForEach(x => { decimal dummy = x; });
}));
回数 long float double decimal
1 00:00:02.9938246 00:00:02.5987278 00:00:02.5854997 00:00:06.6623947
2 00:00:02.1549065 00:00:02.3589645 00:00:02.3423787 00:00:06.6627419
3 00:00:02.1535535 00:00:02.3024205 00:00:02.2941939 00:00:06.6671070



以上