Skip to content

MS_NumericCalculation

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

数値の計算方法

概要

特に小数点を持つ数値の計算を行う場合は、以下の点に考慮する。

桁溢れ

動作

オーバーフロー

  • 扱える数値の最大値を超える
  • オーバーフロー時:System.OverflowException 例外、

アンダーフロー

  • 指数部が小さくなり過ぎる
  • アンダーフロー時:System.UnderflowException 例外が発生する。

移行メモ(正誤): .NET に System.UnderflowException は存在しない
System.OverflowException はあるが、アンダーフローに対応する例外型はない。

実際の挙動は型によって異なる。

アンダーフロー時の挙動
float / double 例外は出ない非正規化数を経て、静かに 0 になる
decimal 例外は出ない。有効桁を超えた分は丸められて 0 になる
整数型 そもそもアンダーフローの概念がない(最小値を下回れば OverflowException または折り返し)
double d = 1e-320;             // 非正規化数(精度は落ちるが値は残る)
Console.WriteLine(d / 1e10);   // 0  ← 例外は出ない

decimal m = 0.0000000000000000000000000001m;  // 1e-28
Console.WriteLine(m / 10m);    // 0  ← 例外は出ない

「静かに 0 になる」ことが、オーバーフローより厄介である。
例外で気付けないため、除算の分母にすると 0 除算になる
累積すると結果が丸ごと消える、といった形で後から表面化する。

コンパイラ オプション

.NET はコンパイラ オプションでオーバーフロー、アンダーフローのチェックが可能である。

移行メモ(チェックの対象は整数だけ): コンパイラ オプションの
/checked(C#)や /removeintchecks(VB)が効くのは、
整数型の演算だけである。

【チェックされる】 int, long, short, byte, uint, ulong … の
                   +, -, *, ++, --, 明示的な数値変換

【チェックされない】float, double  ← 仕様上、例外を出さない
                   decimal        ← 常にチェックされる(オプション不問)

つまり、原文が言う「アンダーフローのチェック」は
コンパイラ オプションでは実現できない
浮動小数点のアンダーフローは、自分で値域を検査するしかない。

現在の書き方.csproj):

<PropertyGroup>
  <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>

既定は false(チェックしない = 折り返す)。
金額や数量を扱うなら true にしておく方が安全である
(性能への影響はごく小さい)。

キーワード / ステートメント

コンパイラ オプションで設定したオーバーフロー、アンダーフローのチェックの既定の動作を変更したい場合は、

補足(checked / unchecked の使い分け): 既定(unchecked)では
静かに折り返すため、バグが見えなくなる。

int max = int.MaxValue;

unchecked { Console.WriteLine(max + 1); }  // -2147483648 ← 折り返す
checked   { Console.WriteLine(max + 1); }  // OverflowException
場面 選択
業務計算(金額・数量・件数) checked。誤った値が流れる方が危険
ハッシュ計算、CRC、ビット演算 unchecked。折り返しが仕様の一部
性能が極限まで要る内側のループ unchecked(ただし測ってから)

Option Strict On は VB では必須と考えてよい。
暗黙の型変換を止めることで、桁溢れ以前に
意図しない型変換そのものを防げる。

小数点を持つ値の計算

問題

丸め誤差

  • 切り捨て
  • 四捨五入
  • 切り上げ

補足(.NET の既定は「銀行丸め」): Math.Round の既定は
四捨五入ではない。ここは実務でよく事故る。

Math.Round(0.5)   // 0  ← 1 ではなく 0(偶数側へ丸める)
Math.Round(1.5)   // 2
Math.Round(2.5)   // 2  ← 3 ではない

// 四捨五入したいなら明示する
Math.Round(0.5, MidpointRounding.AwayFromZero)   // 1
Math.Round(2.5, MidpointRounding.AwayFromZero)   // 3

既定の MidpointRounding.ToEven(銀行丸め・偶数丸め)は、
大量の丸めを行った際に誤差が一方向に偏らない
利点がある。
一方、日本の商習慣や税計算は四捨五入(あるいは切り捨て)を前提
にしていることが多い。

メソッド 動作
Math.Round(x) 銀行丸め(既定)
Math.Round(x, MidpointRounding.AwayFromZero) 四捨五入
Math.Floor(x) / Math.Truncate(x) 切り捨て(負数で挙動が違う
Math.Ceiling(x) 切り上げ

Floor は「小さい方へ」、Truncate は「0 の方へ」
Math.Floor(-1.5)-2Math.Truncate(-1.5)-1 である。

仕様書に丸め方を明記させ、コードでも明示する——
これが唯一の対策である。

打ち切り誤差

0.3333....(無限級数)を有限項で打ち切った場合。

桁落ち

ほぼ等しい 2 つの数値の差を求める場合、
計算結果が 0 に極端に近くなり有効桁数が減る。

情報落ち

非常に大きな数値と小さな数値の加減算で、
小さな数値が結果に反映されない。

補足(誤差の実例): 挙げられた 4 つは、どれも実際に見える

// ① 丸め誤差(2 進数で 0.1 を表現できない)
Console.WriteLine(0.1 + 0.2 == 0.3);          // False
Console.WriteLine((0.1 + 0.2).ToString("R")); // 0.30000000000000004

// ② 桁落ち
double a = 1.0000000001, b = 1.0000000000;
Console.WriteLine((a - b).ToString("R"));     // 1.000000082740371E-10(有効桁が激減)

// ③ 情報落ち
double big = 1e16, small = 1.0;
Console.WriteLine(big + small == big);        // True ← small が消えた

// ④ 加算順序で結果が変わる
Console.WriteLine(1e16 + 1 + 1 - 1e16);       // 0
Console.WriteLine(1 + 1 + 1e16 - 1e16);       // 2

④ は特に重要で、浮動小数点の加算には結合則が成り立たない
大量のデータを合計する際は、小さい値から足す、あるいは
Kahan の補正加算を使うと誤差を抑えられる。

== で浮動小数点を比較しないのが鉄則である。

// ✗
if (x == y) { }
// ○ 許容誤差を決めて比較する
if (Math.Abs(x - y) < 1e-9) { }

Decimal

特に小数点を持つ金額の計算には、Decimal を用いる。

Decimal の特徴を以下に示す。

Decimalの特徴

  • ほとんどの 10 進数の小数は 2 進数(Single、Double 型などの浮動小数点型)で表現することができない。

    • このような値は 2 進数では近似値でしか表現できず、その誤差が計算結果として現れる(打ち切り誤差)。
    • ただし、k/(2^n)(k と n は整数)で表すこともできる小数は 2 進数でも表現できる。
  • Decimal は浮動小数点型と比べ、金額計算の慣例に適合する。

    • 10 進数を表現する。
    • 有効桁数が多く、範囲が狭い(28 ~ 29)。
    • 格納できる値の大きさの範囲が狭いは、金額の計算には十分。
  • 問題点は、計算速度が遅いこと。

補足(型の選択): 原文の主張——
金額計算は decimal——は現在も完全に正しい。数値で裏付けておく。

float double decimal
内部表現 2 進浮動小数点 2 進浮動小数点 10 進浮動小数点
サイズ 4 バイト 8 バイト 16 バイト
有効桁数 約 7 桁 約 15~17 桁 28~29 桁
範囲 ±1.5e−45 ~ ±3.4e38 ±5.0e−324 ~ ±1.7e308 ±1.0e−28 ~ ±7.9e28
0.1 の表現 不可(近似) 不可(近似) 可(正確)
速度 速い(CPU が直接扱う) 速い 遅い(10~20 倍程度)

使い分け:

用途
金額、税、数量、率(10 進で定義される値) decimal
科学技術計算、統計、座標、物理量 double
大量データ・機械学習・グラフィックス float(精度より量)
通貨を整数で持つ(最小単位で保持) long最速かつ正確。円なら丸め不要)

decimal も万能ではない点に注意する。

decimal d = 1m / 3m;        // 0.3333333333333333333333333333(打ち切り誤差は残る)
Console.WriteLine(d * 3m);  // 0.9999999999999999999999999999 ← 1 にならない

除算がある限り誤差はゼロにならない
「どこで丸めるか」を業務ルールとして決めることが本質である。

DB との対応SQL Server):

.NET SQL Server
decimal decimal(p,s) / numeric(p,s) / money
double float
float real

**decimal の精度は SQL Server(38 桁)より狭い(28~29 桁)**ため、
decimal(38,10) のような列を読むと OverflowException になり得る。

参考資料

補足(現在追加された型): 原文の時代にはなかった型・機能がある。

型 / 機能 内容
System.Half 2 バイトの浮動小数点(.NET 5~)。機械学習向け
System.Int128 / UInt128 128 ビット整数(.NET 7~)
System.Numerics.BigInteger 桁数無制限の整数。正確だが遅い
System.Numerics.Complex 複素数
ジェネリック数値演算 INumber<T> 等(.NET 7~)。型を跨いだ数値アルゴリズムが書ける
// .NET 7 以降:型に依らない合計
static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
    => values.Aggregate(T.Zero, (a, b) => a + b);

参考

Microsoft Learn


Tags: 移行, プログラミング, .NET開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally