小数を含む実数を固定小数点数形式や浮動小数点数形式で 2 進数化すると大抵の場合丸め誤差が生じます。
この丸め誤差を防ぐ方法は存在しないのですが、コスト面を気にしなければ精度を上げてみたり(例えば単精度型を使っている場合は倍精度型にするなど)、計算方法を工夫すると丸め誤差による影響を小さくすることができます。
なお丸め誤差は計算を繰り返すことによってどんどん蓄積されていくので計算結果が想定した値とは全く異なることがあります。
よって
「固定小数点数や浮動小数点数を扱う場合は条件式などで同値判定( == )をしてはいけません」
※ たまたま上手く行くときもありますが、大抵の場合は誤作動してバグの元になります。
例えば以下のソース 1 は a = 1.0 から 0.1 を 10 回引くというプログラムの例です。
計算結果は当然 0 になるはずですが、コンピュータでこの計算をすると for ループを繰り返す度に丸め誤差が蓄積されいきます。
よって条件式(a == 0) が真にならないため「aはゼロではありません」と表示されます。
ソース 1: 浮動小数点数を扱う場合に同値判定( == )をしては駄目な例 #include <stdio.h> int main() { float a = 1.0; for( int i=0; i<10; ++i) a -= 0.1; printf("a=%lf\n",a); if( a == 0 ) printf("aはゼロです\n"); else printf("aはゼロではありません\n"); return 0; } 結果: a=-0.000000 aはゼロではありません
この様な問題がありますので、浮動小数点数で同値判定をしたい場合は次のように差分の絶対値がある範囲に入ったかどうかで判定します
a を浮動小数点数の変数
A を同値判定したい数字
e を十分小さい正の数字
としたとき
|a-A| < e
プログラムの条件式で書くと
( -e < a-A && a-A < e )
上の e の値は正式には「計算機イプシロン」という特殊な数字を使うのですが、実用上はプログラマーが適当に値を決めることも多いです。
※ あまり適当に決め過ぎるとこれまたバグのもとになるので注意が必要です
丸め誤差とは違う浮動小数点数形式特有の誤差として「桁落ち」があります。
桁落ちといういうのは、「近い値の小数同士で引き算をすると仮数部の有効桁数が減って情報を損失する」という現象のことです。
今回は極端な例として
$\sin ( 10.0000001 ) - \sin ( 10 ) $
という計算を考えてみましょう。
まず手元にある関数電卓などを使って計算すると $\sin ( 10.0000001 )$と$\sin ( 10 )$ は近い値同士なので
$\sin ( 10.0000001 ) - \sin ( 10 ) = -0.000000083907149672768355230801\cdots$
となり、計算結果が無限小数かつ相当小さい値になることが分かります。
そこで今度は単精度型で $\sin ( 10.0000001 )$ を計算した結果を 2 進数で表してみます。
すると
符号部 = 1
指数部 = 01111110
仮数部 = 00010110100010011111001
※ 16 進数だと 0x BF 0B 44 F9
と変換されるため、仮数部の有効桁数は 23 桁あることが分かります。
同様に単精度型で $\sin ( 10 )$ を計算した結果を 2 進数で表すと
符号部 = 1
指数部 = 01111110
仮数部 = 00010110100010011111000
※ 16 進数だと BF 0B 44 F8
となり、(下3桁が0なので)仮数部の有効桁数は 20 桁あることが分かります。
最後に単精度型で $\sin ( 10.0000001 ) - \sin ( 10 )$ を計算した結果を 2 進数で表すと
符号部 = 1
指数部 = 01100111
仮数部 = 00000000000000000000000 ← 0 になった!
※ 16 進数だと 0x B3 80 00 00
※ 10 進数だと -0.000000059604644775390625
となり、本来は無限に桁があるはずの仮数部の有効桁数が 0 桁まで減ります。
言い換えると「仮数部が持っていた情報が消滅した」ということを意味します。
上の例はやや極端な例でしたが、どの様な計算をしても多かれ少なかれ桁落ちが生じます。
ただし丸め誤差と同様に、精度を上げてみたり、計算手順を工夫すると桁落ちによる影響を小さくすることができます。