導入
以前に書いた各レベル毎の鉱脈採掘の損失率を求めるプログラムですが、
(define (vein level speed ratio)
(if (< ratio 0.0001)
'done
(begin
(print (format #f "level = ~3,'0d, speed = ~4,'0d, ratio = ~1,4f" level speed ratio))
(vein (+ level 1) (+ speed 10) (* ratio 0.94)))))
小数点以下 4 桁目までを算出しようとすると、
(略)
level = 184, speed = 1940, ratio = 0.0011
level = 185, speed = 1950, ratio = 0.0011
level = 186, speed = 1960, ratio = 0.0010
level = 187, speed = 1970, ratio = 9.4383e-4
level = 188, speed = 1980, ratio = 8.8720e-4
level = 189, speed = 1990, ratio = 8.3397e-4
level = 190, speed = 2000, ratio = 7.8393e-4
(略)
このように表示が乱れてしまいます。
単に表示の乱れだけかも知れませんが、良いと言えるコードではないので根本的に直します。
アンダーフロー
小さな数同士を演算すると正しく演算ができない状態に陥る事があります。コンピュータは、とても小さな値を正確に表現することが難しいためです。これを「アンダーフロー」と言います。
解決方法
とても小さな数を扱ったり、とても小さな値同士を演算することは、コンピュータは得意でないので、適切な値に大きくしてあげれば良いのです。一番簡単な方法は浮動小数点数を整数として扱う方法です。
(define (vein level speed ratio)
(define (iter lvl spd rto)
(if (< rto 1)
'done
(begin
(print (format #f "level = ~3,'0d, speed = ~4,'0d, ratio = 0.~5,'0d" lvl spd rto))
(iter (+ lvl 1) (+ spd 10) (x->integer (floor (* rto 0.94)))))))
(iter level speed (x->integer (* ratio 100000))))
引数つまりンターフェース部分はそのままに、プログラム内部では浮動小数点数を 10 万倍することで、整数として扱うようにします。
level = 180, speed = 1900, ratio = 0.00137
level = 181, speed = 1910, ratio = 0.00128
level = 182, speed = 1920, ratio = 0.00120
level = 183, speed = 1930, ratio = 0.00112
level = 184, speed = 1940, ratio = 0.00105
level = 185, speed = 1950, ratio = 0.00098
level = 186, speed = 1960, ratio = 0.00092
level = 187, speed = 1970, ratio = 0.00086
level = 188, speed = 1980, ratio = 0.00080
level = 189, speed = 1990, ratio = 0.00075
level = 190, speed = 2000, ratio = 0.00070
level = 191, speed = 2010, ratio = 0.00065
level = 192, speed = 2020, ratio = 0.00061
level = 193, speed = 2030, ratio = 0.00057
level = 194, speed = 2040, ratio = 0.00053
level = 195, speed = 2050, ratio = 0.00049
level = 196, speed = 2060, ratio = 0.00046
level = 197, speed = 2070, ratio = 0.00043
level = 198, speed = 2080, ratio = 0.00040
level = 199, speed = 2090, ratio = 0.00037
level = 200, speed = 2100, ratio = 0.00034
level = 201, speed = 2110, ratio = 0.00031
level = 202, speed = 2120, ratio = 0.00029
level = 203, speed = 2130, ratio = 0.00027
level = 204, speed = 2140, ratio = 0.00025
level = 205, speed = 2150, ratio = 0.00023
level = 206, speed = 2160, ratio = 0.00021
level = 207, speed = 2170, ratio = 0.00019
level = 208, speed = 2180, ratio = 0.00017
level = 209, speed = 2190, ratio = 0.00015
level = 210, speed = 2200, ratio = 0.00014
level = 211, speed = 2210, ratio = 0.00013
level = 212, speed = 2220, ratio = 0.00012
level = 213, speed = 2230, ratio = 0.00011
level = 214, speed = 2240, ratio = 0.00010
level = 215, speed = 2250, ratio = 0.00009
level = 216, speed = 2260, ratio = 0.00008
level = 217, speed = 2270, ratio = 0.00007
level = 218, speed = 2280, ratio = 0.00006
level = 219, speed = 2290, ratio = 0.00005
level = 220, speed = 2300, ratio = 0.00004
level = 221, speed = 2310, ratio = 0.00003
level = 222, speed = 2320, ratio = 0.00002
level = 223, speed = 2330, ratio = 0.00001
done
修正後のプログラムではアンダーフローを起こさずに、小数点以下 5 桁目まできちんと表示することができます。
内部のしくみ
コンピュータの CPU 内には「レジスタ」と呼ばれる、超高速で動作する極めて少量の記憶領域があります。コンピュータで計算を行う時は、必ずこのレジスタに値を入れる必要があります。計算をするには、計算したい値をレジスタに格納し、演算命令を実行します。計算結果もレジスタに格納されます。レジスタの値はそのままでは人間の目には見えないので、値を文字としてレンダリングしディスプレイに表示しています。
現在ではこのレジスタは大抵 64 ビット長のデータを保持できます。通常の整数はそのまま二進数で表現しますが、浮動小数点数の場合は IEEE で定めた形式で保持するのが普通です。たくさんのビット数を使うことで、より小さな(大きな)値を表現できるようになった事は確かですが、有限のビット数であることに変わりはありません。ですので、浮動小数点数を表現するのにも限界があり、値が丸められることで誤差が生じたり、正しく演算できなくなったり(アンダーフロー)することがあります。
逆に、大きな数についても同様に正しく演算できない事があります。(大きな数同士の演算が正しく行われないことを「オーバーフロー」と言います)
コンピュータの内部のしくみについて詳しく知りたい人は、「アセンブラ」を勉強すると良いでしょう。コンピュータや動作原理を深く理解することが出来るでしょう。
まとめ
とても小さい数を扱う時は、コンピュータが演算しやすいように値を大きくしてみましょう。浮動小数点数をそのまま扱うのでなく整数化することを考えましょう。コンピュータは浮動小数点数よりも整数で計算を行うほうが得意です。また、プログラムのインターフェイス(引数)は変えずに内部だけ工夫して実装しましょう。(これを「リファクタリング」と言います)