Let's 'C#で数値計算' with Mol & Dsl

C# 数値計算の基本事項:データ型と誤差

C# での主要なデータ型及び数値のとり得る範囲

以下は C# で標準的に使用される「整数型」と「実数型」のデータ型及び数値のとり得る範囲の一覧です。
予約語別名意味最小値最大値有効桁数
sbyteSystem.SByte符号付き8ビット整数-1281273
byteSystem.Byte符号なし8ビット整数02553
shortSystem.Int16符号付き16bit整数-32768327675
ushortSystem.UInt16符号なし16bit整数0655355
intSystem.Int32符号付き32bit整数-2147483648214748364710
uintSystem.UInt32符号なし32bit整数0429496729510
longSystem.Int64符号付き64bit整数-9223372036854775808922337203685477580719
ulongSystem.UInt64符号なし64bit整数01844674407370955161520
floatSystem.Single単精度実数-3.402823E+383.402823E+387
doubleSystem.Double倍精度実数-1.79769313486232E+3081.79769313486232E+30815~16
decimalSystem.Decimal10進型-792281625142643375935439503357922816251426433759354395033528~29
データ型は整数を扱う「整数型」と小数点以下の数値も扱える「実数型」に分類できます。さらに「実数型」でも float と double は「2進実数型」、decimal と BigDecimal は 「10進実数型」に分類されます。

2進実数型(float、double)は以下のプロパティを持ちます。
プロパティ説明
Epsilonゼロより大きい最小値
MaxValue最大有効値
MinValue最小有効値
NaN 非数(NaN)
NegativeInfinity負の無限大
PositiveInfinity正の無限大
decimal と整数型には MinValue と MaxValue はありますが、それ以外の 無限や非数の定義はありません。
BigDecimal には MinValue と MaxValue はありませんが、無限や非数の定義はあります。

実際に表示すると、以下のようになります(// ==> の右に結果が記述されています)。


 void W(string st)
 {
    Console.WriteLine(st);
 }
※プログラム例は文字列を出力するために、上記のようなメソッド用いています。

[2進実数型]

  W("" + double.Epsilon);          // ==>  4.94065645841247E-324
  W("" + double.MaxValue);         // ==>  1.79769313486232E+308
  W("" + double.MinValue);         // ==> -1.79769313486232E+308
  W("" + double.NaN);              // ==> NaN (非数値)
  W("" + double.NegativeInfinity); // ==> -∞
  W("" + double.PositiveInfinity); // ==> +∞
[整数型]

  W("" + int.MinValue);   // ==> -2147483648
  W("" + int.MaxValue);   // ==>  2147483647
[2進実数型の特殊な計算例]

  W("" + (0.0/0.0));                                           // ==> NaN (非数値)
  W("" + (1.0/0.0));                                           // ==> +∞
  W("" + (double.PositiveInfinity + double.PositiveInfinity)); // ==> +∞
  W("" + (double.NegativeInfinity + double.PositiveInfinity)); // ==> NaN (非数値)
  W("" + (double.NegativeInfinity + double.NegativeInfinity)); // ==> -∞
  W("" + (double.MinValue * 2.0));                             // ==> -∞
  W("" + (double.MaxValue * 2.0));                             // ==> +∞
  W("" + (1.0 + double.NaN));                                  // ==> NaN (非数値)
  W("" + (1.0 == double.NaN));                                 // ==> False
  W("" + (double.NaN == double.NaN));                          // ==> False
  W("" + (double.NaN != double.NaN));                          // ==> True
[整数型の特殊な計算例]

  int m = 2;
  int z = 0;
  W("" + (int.MinValue * m));  // ==>  0
  W("" + (int.MaxValue * m));  // ==> -2
  W("" + (m / z));             // ==> 例外発生(追加情報: 0 で除算しようとしました。)
※重要 ±∞ や NaN が出現するのは何らかの計算ミスに遭遇したことを意味しますが、計算が続行されてしまうので エラー場所の特定が面倒になります。 BigDecimal は計算を続行するか、例外を発生させるのかを選択することができます。

丸め誤差

入力数値 0.1 等を取り込む場合や、 1/3 等の計算で循環数に落ちった場合に計算を打ち切った (四捨五入等の丸め操作を伴う)場合に生ずる誤差です。以下のプログラムは 0.5 は2進に変換できるのに 0.1 は変換できないことによる結果を示しています(1.0 や 5.0 の整数は変換できます)。

  double pt1 = 0;
  double pt5 = 0;
  decimal d1 = 0m;
  for (int i = 0; i < 10; ++i)
  {
     pt1 += 0.1;
     d1  += 0.1m;
     pt5 += 0.5;
  }
  W("" + (pt1 == 1.0));  // ==> False
  W("" + (d1 == 1.0m));  // ==> True
  W("" + (pt5 == 5.0));  // ==> True
10進数と2進数は整数なら相互に変換可能です。 しかし、1以下の数値の多くは変換できず循環小数になります。 2進数の小数値は

2進数の小数値 = 0.d1d2d3...
と表記されます。ここで di は 0 か 1 の数字です。 10進数値に変換する計算式は

2進数の小数値: 0.d1d2d3... <=> d1/21 + d2/22 + d3/23...:10進数値
となります。 従って

10進数の 0.5 = 1/2 => 0.1 (2進)
と変換できます。しかし、

 2-1 => 0.5
 2-2 => 0.25
 2-3 => 0.125
 2-4 => 0.0625
 2-5 => 0.03125
 ................
なので、確かめてみれば(有効桁は右に伸び続ける…)わかりますが、

0.1(10進) => 0.0625 + 0.03125 + ……  => 0.0001100110011……(2進)
となってしまい、10進の 0.1 は2進に変換できません(無限に繰り返す循環小数になるので、最大桁位置で丸めることになります)。 10進の 1/2、1/4、1/8…等は(割り切れるので)2進に変換可能です。 開始から順に足しながら最終値に達する微分方程式の積分刻みなどは、 1/2、1/4、1/8…等が安全です。

※decimal や BigDecimal は10進で(ソフトウェアによる)計算を実行するため、このような変換誤差は生じません。

情報落ち

double でも高々 16 桁程度しか保持できないので、極端に大きさの違う数を足したり引いたりするときの計算誤差です。 以下のプログラムと実行結果が情報落ちの代表的な例になります。

  double eps = 1.0;
  while (1.0 + eps > 1.0) eps /= 10.0;
  W("eps=" + eps);    // ==> eps=1E-16 (1.0 に 1E-16 以下の値を足しても無駄…)
別の例として、平均値と(不偏)分散の定義は

平均値 = Σni=1 Xi/n
分散   = Σni=1 (Xi-平均値)2/(n-1)
ですが、多くの統計学の教科書では、分散を

二乗和 = Σni=1 Xi2
分散   = (二乗和-n・平均値2)/(n-1)
と計算した方が簡単と記述しています。 しかし、二乗和を計算するときには情報落ちが発生しやすいので定義通りに計算した方が安全です。 同様に、数値列の総和を求める場合などは(可能な限り)小さい数から足していくべきです。

桁落ち

大きさがほぼ同じ数同士の引き算によって、有効桁数が減少することです。 例えば、12345678.9 - 12345678.8 => 0.1 となって有効桁数が 9 桁から 1 桁に減少してしまうことです。 測定値や連続した浮動小数計算の場合、最後の桁は信用できない場合が多いので、この時点で今までの計算は無駄になってしまうことになり得ます。 桁落ち対策例として、以下の2次方程式の根を求める方式が有名です。 これは、-b±(b2-4ac)1/2 の部分で、b2>>4ac の場合も想定して、 引き算を実行せず、桁落ちを防いでいます。

  /// <summary>
  /// 2次方程式 ax**2 + bx + c = 0 の根(x:x1,x2)を計算します。
  /// </summary>
  /// <param name="a">2次項の係数(!=0.0を前提)</param>
  /// <param name="b">1次項の係数</param>
  /// <param name="c">定数項</param>
  /// <returns>解の配列(2要素)、虚数解のときは null </returns>
  double [] QuadraticFormula(double a,double b,double c)
  {
     double  d = b * b - 4 * a * c; // 判別式の計算
     if(d<0.0) return null;         // 解は虚数
     double[] x = new double[2];
     x[0] = (Math.Abs(b) + Math.Sqrt(d)) / (2.0 * a);
     if ( b >= 0.0 ) x[0] = -x[0];
     x[1] = c / (a * x[0]);         // 根と係数の公式より(x[0]*x[1]==c/a)
     return x;
  }