プログラマが知るべき97のこと/浮動小数点数は実数ではない

浮動小数点数は、数学でいうところの「実数」とは違います。PascalやFortranなどプログラミング言語によっては「実数」という名前で呼ばれることもありますが、厳密には実数ではないのです。実数は、まず精度が無限です。完全に連続していて、「桁落ち」もありません。一方、浮動小数点数の精度は有限です。実数というよりは、むしろ整数に近いと言っていいでしょう。いわば「非常に奇妙な整数」です。「非常に奇妙」というのは、数と数の感覚が、通常の整数とは違って均等ではないからです。

たとえば、「2147483647」という数(符号付き32ビット整数の最大値)を、32ビットの浮動小数点型変数(仮にxとします)に代入し、xの値を出力したとします。すると、その結果は「2147483648」となります。x-64を出力しても、結果はやはり「2147483648」になり、x-64を出力すると、結果は何と「2147483520」になってしまいます。なぜでしょうか?それは、この範囲では、数の間隔が「128」になっていて、浮動小数点数の演算では、最も近い数値への「丸め」が行われるためです。

IEEEの規格で定められた浮動小数点数は、基数を2つとした科学的記数法で表現されます。この記数法では、たとえば「​1.d1d2...dp-1×2e​」というふうに記されます。pは仮数部の表現に使われるビット数(単精度では24、倍精度では53)です。2つの隣り合う数の間隔は、21-p+eになります。これはほぼ、ε|x|と考えても大きな間違いはないでしょう。εはマシンイプシロン(21-p)を表します。

浮動小取点数において、数と数の間隔がどのようになっているかを正しく把握しておけば、「演算結果が想定と大きく異なってしまう」という起こりがちな間違いを防ぐことが出来ます。たとえば、方程式の探索などのような反復計算の際、定められた以上の精度を求めるようなコードを書いても意味はありません。数と数の間隔が広すぎれば、永久に求める答えは得られず、無限ループに陥ってしまいます。

浮動小数点数は、実数の近似値です。つまり、誤差が生じることは避けられないということです。「丸め」によって生じる誤差が原因で、演算結果が予測とまったく違って驚くことは珍しくないのです。たとえば、ほとんど同じ大きさの数値どうしの演算をしたとします。その場合、最上位の桁は、互いに相殺し合い、最下位の桁にあった数(丸めによる誤差を含んだ数)が最上位の桁にまで繰り上がる、ということが起きます。この演算結果を踏まえてさらに演算を重ねていくと、誤差はどんとん大きくなっていってしまいます。こういう悲惨な結果を防ぐには、自分の書いたコードのアルゴリズムをよく確認する必要があります。例を1つあげておきましょう。方程式「x2-100000x+1=0」を、二次方程式の解の公式を使って解くとします。この場合、式「-b+sqrt(b2-4)」のオペランドの大きさはいずれもほぼ同じなので、代わりに、根「r1=-b-sqrt(b2-4)」を計算して、その後にr2 = l/r1を求めることができます。二次方程式「ax2+bx+c-0」の場合、根は必ず「r1r2=c/a」を満たします。 誤差が大きくなっていくという現象は、もっと目立たないかたちで起きることもあります。仮に、exを、単純に「1+x+x2/2+x3/3!+・・・」という式で計算するライブラリがあったとします。xが正の数であれば、この方法でも問題は起きないのですが、もしxが絶対値の大きい負の数であったらどうなるかを考えてみましょう。その場合は、偶数番目の項の演算結果は大きな正の数になり、そこから、奇数番目の項の演算結果を差し引くことになります。それだけでは特に誤差が生じるわけではありません。ただ問題は、偶数番目の項の演算結果に対する丸めです。演算結果が大きな数であれば、かなり上位の桁で丸めが行われてしまうのです。このことから、最終的な演算結果は、正の無限大に向かつて大きくなり、本来得られるべき結果とは大きく異なってしまいます。この問題を解決する方法は簡単です。xが負の数の場合はex = 1/e 1x1 を計算するようにすればいいのです。 最後に、金融関係のアプリケーションには浮動小数点数を使うべきではないということも書いておくべきでしょう。pythonやC#に、Decimalクラスが用意されているのはそのためです。浮動小数点数は、元々、科学技術計算を効率的に行うことを目的としたものです。しかし、正確さを欠いていては、効率がいくら良くても価値はありません。浮動小数点数を使うときは、どういう時に丸めの誤差が出るかをよく知り、その知識を踏まえた上でコーディングをすべきです。