C# 数値計算の基本事項:データ型と誤差
C# での主要なデータ型及び数値のとり得る範囲
以下は C# で標準的に使用される「整数型」と「実数型」のデータ型及び数値のとり得る範囲の一覧です。
予約語 | 別名 | 意味 | 最小値 | 最大値 | 有効桁数 |
sbyte | System.SByte | 符号付き8ビット整数 | -128 | 127 | 3 |
byte | System.Byte | 符号なし8ビット整数 | 0 | 255 | 3 |
short | System.Int16 | 符号付き16bit整数 | -32768 | 32767 | 5 |
ushort | System.UInt16 | 符号なし16bit整数 | 0 | 65535 | 5 |
int | System.Int32 | 符号付き32bit整数 | -2147483648 | 2147483647 | 10 |
uint | System.UInt32 | 符号なし32bit整数 | 0 | 4294967295 | 10 |
long | System.Int64 | 符号付き64bit整数 | -9223372036854775808 | 9223372036854775807 | 19 |
ulong | System.UInt64 | 符号なし64bit整数 | 0 | 18446744073709551615 | 20 |
float | System.Single | 単精度実数 | -3.402823E+38 | 3.402823E+38 | 7 |
double | System.Double | 倍精度実数 | -1.79769313486232E+308 | 1.79769313486232E+308 | 15~16 |
decimal | System.Decimal | 10進型 | -79228162514264337593543950335 | 79228162514264337593543950335 | 28~29 |
- float はたった7桁、double でも最大16桁しか保持できません。
- decimal は最大29桁まで保持できますが、指数部分は ±28 の範囲で double (-324~+308)と比べると扱える値範囲は狭くなります。
- decimal は計算も10進です。double 等のように2進に変換する際の誤差は発生しません(0.1m のように 'm' を付けます)。
- Mol の BigDecimal も10進です。有効桁数は無制限、指数部は int の取り得る範囲となります。
- メモリーサイズ・計算時間は float→double→decimal→BigDecimal と大きくなります。
データ型は整数を扱う「整数型」と小数点以下の数値も扱える「実数型」に分類できます。さらに「実数型」でも
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 を含む計算の結果は全て NaN になります。また NaN は自分も含めて全てに一致しません。
従って、数値 x が NaN であるかどうかは double.IsNaN(x) としなければなりません。
x == double.NaN は x がたとえ NaN でも False になります(x != double.NaN は常に True)。
- 整数はゼロで除算すると例外が発生します。その他の計算で例外は発生しません。
- 2進実数の計算はゼロの除算を含めて例外は発生しません。
±∞ や 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;
}