数値計算というのは誤差を含むのが当たり前

no extension

んー。 内容は間違ってないんだけど,なんか違うような。 ちうわけで,ロートル・エンジニアである (おぢさん) が昔語りをしてみよう。

とんとん昔のことじゃった

もともとコンピュータのプロセッサは小数点数の数値を扱えるように出来ていなかったし,もっと言うと昔のプロセッサの中には整数の割り算のインストラクションすらなかったものもあった。 故に小数点数を扱うものや割り算を含む計算は必然的に高コストになり,これを如何にして減らすかがプログラマの腕の見せ所であったのだ。

当時,コンピュータで小数点数を扱うための戦略は大まかに2つあった。

  1. 桁を合わせて固定小数点数(fixed-point number)として扱う
  2. 浮動小数点数(floating point number)を扱えるコプロセッサまたは計算ライブラリを利用する

前者の固定小数点数は小数点が置かれる桁を固定して表した数値型で,整数の演算とほぼ同じコストで演算できるのが特徴である1

後者の浮動小数点数は内部表現として仮数部と指数部をもつ数値型である。 たとえば $123.45$ は $1.2345 \times 10^{2}$ という表現をとる。 $1.2345$ が仮数部の値で $10^{2}$ の $2$ が指数部の値である。

ただしいずれも10進数(基数が10)の場合。

浮動小数点数型として一般的なコンピュータ・システムで使われる IEEE 754 規格では仮数部も指数部も2進数(基数が2)で表される。 固定小数点数でも小数部に Q フォーマットを使う場合は2進数(基数が2)表現である2。 たとえば10進数の $0.1$ を2進数で表すと $0.000110011\dots$ と循環小数になるため必ず誤差が発生してしまうわけだ。

他にも,固定小数点数にしろ浮動小数点数にしろ,型のサイズが決まっている3 ため数値の精度によっては情報落ちなどの誤差が発生する。

数値計算というのは誤差を含むのが当たり前

日常的に10進数を使う人間から見て,なぜ浮動小数点数のような不完全な数値型が今でもまかり通るのか。 それは数値計算というのは誤差評価を含むものだからである。 たとえば $123.45$ という入力値の評価が $123.45 \pm 0.01$ なのか $123.450 \pm 0.001$ なのかでロジックが変わるかも知れない(誤差は伝搬する)。

しかし,こういった誤差評価は数値型が何であれ(たとえ手計算でも)必要なことであり,誤差に関する設計を怠ったままテキトーにコードを組んでも吐き出される結果は信用できないということになる。

私は誤差論を大学に入ってから学んだのだが,最近の人はどのタイミングで学ぶのか。 大系としての誤差論をすっ飛ばして近視眼的に浮動小数点数の計算誤差が云々というのは,どうにも納得しがたいものがある。

計算誤差を許容できない場合

とはいっても基数の差異による誤差や情報落ちなどの計算誤差が許されない場合もある。 たとえばお金の計算で「1を1億回足して1億にならない」と困るよね。 お金勘定のシステムで「float や double なんか使うな」などと言われた人もいるかも知れない。

そういう場合は浮動小数点数型を使うのではなく各プログラミング言語が用意する特殊な数値型を使う。 たとえば Java では BigDecimal クラスが用意されている。 こういった型やクラスでは任意の精度と10進数に対応した計算ロジックが組み込まれており,浮動小数点数演算で見られるような計算誤差を避けられるようになっている。

デメリットとしては浮動小数点数演算よりも計算コストが高くなることだろうか。 メリットとデメリットのバランスを見て上手く組み込んでほしい。

Go 言語の場合

ちなみに Go 言語には math/big パッケージが標準ライブラリにあるのだが4,以下の2つの戦略がとれる。

  1. big.Float 型を使う
  2. big.Rat 型を使う

前者は浮動小数点数なのだが任意の精度を指定することができる。 後者は任意精度の有理数で, $\textstyle\frac{b}{a}$ の内部表現をとる。 扱う数値が有理数のみと言えるなら big.Rat 型がいいだろう。


  1. 小数部の内部表現としてよく使われる Q フォーマットでの加減算は整数の計算と同じだが乗除算は(小数点位置がずれるので)シフト演算を伴う。 ↩︎

  2. 固定小数点数のバリエーションとして10進数の内部表現をとる貨幣(money)型などもある。 ↩︎

  3. IEEE 754 の単精度(32ビット)浮動小数点数は仮数部が23ビット・指数部が8ビットである。倍精度(64ビット)なら仮数部が52ビット・指数部が11ビットとなる。他にも Google の TPU (Tensor Processing Unit) の bfloat16 は仮数部が7ビット・指数部が8ビットになってるらしい。精度より速度を取ったのかな。 ↩︎

  4. 標準ライブラリ以外であれば shopspring/decimal のようなパッケージを公開している人もいる。 ↩︎