re:0.01を100回加算しても1にならない?それともなる?

きのうのエントリへの自己フォロー。ちょっとテストPを書いてみた。自分に課した制限時間は10分! がんばれ俺!

Module Module1
   Sub Main()
       Dim denominator As Decimal = 1
       Dim slicednumber As Single
       Dim sum As Single = 1
       Do While sum = 1        '1をdenominatorで割ったものを、denominator分だけ繰り返し加算したものが1になる間
           denominator += 1    'denominatorを1づつ増やしながらどこで誤差を検知するのかを調べる。1は自明なのでその次から。
           slicednumber = 1 / denominator
           sum = 0
           For cnt As Decimal = 1 To denominator   '割った数だけ割った値を繰り返し加算するループ
               sum += slicednumber
           Next
       Loop
       System.Console.WriteLine("An error detected when denominator is {0}", denominator)
       System.Console.WriteLine("sum is {0}", sum.ToString("e10"))
   End Sub
End Module

ランゲージはVisual Basic 2008。英語のつづりと書式指定がわかんなくて8分ぐらいかかった(遅)。実行結果は

An error detected when denominator is 10
sum is 1.0000001200e+000

正解は「ならない!」というか0.1でダメかよ!
やはり二進数の丸めは侮れない。でも3で割った時点で有限桁で実数を表現しようとした時のアンダーフローが真っ先に発生する。Singleでそれを検知できないのは精度が悪すぎるから。

ちなみにSingleをDoubleにすると6でだめになる。

An error detected when denominator is 6
sum is 1.00000000000000020000e+000*1

一瞬ええっ!っと思うけど、誤差を検知できるだけ精度が向上することによって手前でストップするようになるので、これで合っている。まあ、1/6は循環小数なのでおかしくなるのもあたりまえ。じゃあ、10進数で絶対割り切れている10^−nだとどうなるか? 精度をDoubleのままちょっとプログラムを修正*2して実行すると。

An error detected when denominator is 10
sum is 9.99999999999999890000e-001

それでも1/10、0.1が正確に表現できないことが判明。なんというボロさか! デジタルコンピュータは0.1さえも正確に表現できない!!!!

と、世をはかなんで死のうかと思ったけど、実は.NET Frameworkにはこの辺の問題に関しては最強の型がある。Decimal。

今日この調査をやるまではDecimalは単なる整数型だと思い込んでいた。半分は当たっているんだけど半分は間違いで、確かに内部表現、浮動小数点数仮数部にあたる部分が96ビットある整数なんだけど、その整数の小数点の位置が自由自在に変更できるという特徴がある。小数の演算でも、小数点の位置を右端まで移動したときに、その値が(2^96)−1までの範囲内で表現できるなら実質整数の演算になるので誤差は生じないということになる。オーバーフローするなら終了だけど。

いや、ほんと、金融の数理計算にはCOBOLが最強だと改めて実感。というか、なんで一番基本の型としてそーゆーDecimalみたいなのが普及していないんだ? JavaとかDecimalみたいな型あるんだっけ?? 明確な目的意識を持って設計されたCOBOLというものに、いや、COBOLを作った人たちに畏敬の念を感じた日だった。

*1:ToStringの引数をe20に変更

*2:denominator += 1をdenominator *= 10に修正