#author("2017-09-27T10:26:41+09:00","default:tutimura","tutimura")
#author("2020-11-09T10:59:10+09:00","default:tutimura","tutimura")
[[Cygwinでデバッグ]]
#contents

** 浮動小数点の誤差 [#za5dd748]
C言語では、変数の「型」を気にします。なるべくintで済むところはintで済ませ、どうしても小数の必要な所に限ってdoubleを使う、というのが長年の習慣になっています。このため、意図せず(整数同士の割り算などを行なって)小数部分が0になってしまうという事故が起こり得ます。それなら最初からすべてdoubleを使っておけば安全かというと、そんなに単純な問題ではありません。

浮動小数点演算には誤差がつきものです。従って、ループ変数に浮動小数点型を用いると、予期せぬ事故が起こります。次のプログラムを見てください。10回ループを、int, float, doubleの3通りで実現したつもりです。しかし、本当に10回ループになっているでしょうか。
 #include <stdio.h>
 
 int main(void)
 {
     int i;
     float f;
     double d;
 
     // int で10回ループ
     for (i=0; i<10; i+=1) {
         printf("%d\n", i);
     }
     printf("\n");
 
     // float で10回ループ
     for (f=0.0; f<1.0; f+=0.1) {
         printf("%f = %.20f\n", f, f);
     }
     printf("\n");
     
     // double で10回ループ
     for (d=0.0; d<1.0; d+=0.1) {
         printf("%f = %.20f\n", d, d);
     }
     printf("\n");
     
     return 0;
 }
ある環境での実行例は次のようになりました。(環境依存でこうならない場合もあります。)
 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
 
 0.000000 = 0.00000000000000000000
 0.100000 = 0.10000000149011611938
 0.200000 = 0.20000000298023223877
 0.300000 = 0.30000001192092895508
 0.400000 = 0.40000000596046447754
 0.500000 = 0.50000000000000000000
 0.600000 = 0.60000002384185791016
 0.700000 = 0.70000004768371582031
 0.800000 = 0.80000007152557373047
 0.900000 = 0.90000009536743164062
 
 0.000000 = 0.00000000000000000000
 0.100000 = 0.10000000000000000555
 0.200000 = 0.20000000000000001110
 0.300000 = 0.30000000000000004441
 0.400000 = 0.40000000000000002220
 0.500000 = 0.50000000000000000000
 0.600000 = 0.59999999999999997780
 0.700000 = 0.69999999999999995559
 0.800000 = 0.79999999999999993339
 0.900000 = 0.89999999999999991118
 1.000000 = 0.99999999999999988898
いずれもループ変数の値を表示しているのですが、floatでは、ちゃんと10回ループしているのに、より精度の高いdoubleでは11回ループしています。これはどういうことでしょうか。

ループ変数の値を、小数以下20桁まで表示させると、誤差を含んでいる様子がわかります。0.1 という値は2進数では循環小数になってしまいます。多くのコンピュータの内部表記では、floatやdoubleは2進数で記憶し、有効桁で打ち切られますので、誤差を含んだ近似値として扱うことになります。誤差を含んだ 0.1 を何度も足し合わせると、運が良ければ誤差が打ち消し合って真の値に近くなることもありますが、逆に誤差が拡大していくこともあり、また誤差がプラスに現れるのかマイナスに現れるのかも運次第です。0.1を10回足しあわせて1.0と比較した時に、floatの場合はたまたま1.0より微妙に大きくなったので、都合よくループが終了し、doubleの場合はたまたま1.0より小さかったので、余分にループを回ってしまったということです。
ループ変数の値を、小数以下20桁まで表示させると、誤差を含んでいる様子がわかります。0.1 という値は2進数で表すには循環小数になるので、無限の有効精度が必要になってしまいます。多くのコンピュータの内部表記では、floatやdoubleは2進数で記憶し、有効桁で打ち切られますので、誤差を含んだ近似値として扱うことになります。誤差を含んだ 0.1 を何度も足し合わせると、運が良ければ誤差が打ち消し合って真の値に近くなることもありますが、逆に誤差が拡大していくこともあり、また誤差がプラスに現れるのかマイナスに現れるのかも運次第です。0.1を10回足しあわせて1.0と比較した時に、floatの場合はたまたま1.0より微妙に大きくなったので、都合よくループが終了し、doubleの場合はたまたま1.0より小さかったので、余分にループを回ってしまったということです。

したがってfloatを使えば大丈夫、というわけでもありません。doubleもfloatも10回ループする保証はなく、整数型を使う必要があります。
> doubleやfloatでも、1.0ずつの足し算は誤差なく行える場合があります。ただし数値の絶対値が大きいと、有効桁が足りなくなって1.0の足し算が反映されなくなることもあります。例えば 1.0e+100(10の100乗)に 1.0 を加えても 1.0e+100 のままです。やはりループ変数には浮動小数点型は勧められません。

これ以外にも、C言語では配列の添字には整数型を用いる必要があるという制約もあります。つまり a[5.0] はエラーになります。このことも整数型を多用する理由の1つになっています。
> コンピュータの能力が貧弱であった頃、floatやdoubleの演算速度がintの1000倍も遅い、というような時代もありました。今ではdouble用の演算回路がCPUに多数載せられているので、速度差は数倍程度に縮まっていて、速度が理由でintを用いるような場面は減っています。

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS