第4回 プログラミングについて 『バグ』

『バグ』
今回はバグについて考えてみましょう。
プログラムを作った方なら誰でも必ず経験があるバグ。一発で完全に動作してしまうと疑ってしまいたくなるほど、どんなに気をつけていてもバグは疫病神のようについてまわります。プログラムをダウンさせてしまう致命的なバグ、目的とした結果を出さないバグ、いつも必ず発生するバグ、たまに発生するバグ、果てはコンパイラのバグなど様々です。
いつも必ずプログラムをダウンさせてしまうバグは原因と発生場所を発見することは簡単で、対処もしやすいものです。癖の悪いバグはごくまれに発生するもので、それもバグかどうかも判断しにくいほどの食い違いを発生するものです。数年間使い続けたプログラムに発生することもあります。再現性があるバグは発見しやすいものですが、そうでないものはバグがあるのを知っていても使用し続け、長い間の傾向から判断してバグの発生場所をつきとめるということもあります。
デバッグをするときに大切なことは、バグの真犯人を見つけ出すことです。大きなプログラムになればなるほど真犯人を見つけずらいものです。動作不良の現象が起こっている場所に注目しすぎるあまり、目撃者や被害者をそれとしてしまうことがあるのです。その結果つじつまを合わせをしてなおさらおかしなプログラムにしてしまうことが少なくありません。例えば、
main()
{
int data[10];
int i;
:
:
for(i=0;i<=10;i++)sub(i,&data[i]);
:
:
}
というプログラムは正常に動作するか否かは不確定です。A社のコンパイラでコンパイルすると問題がでず、B社のものだと異常を起こすなどということがあります。バグの原因は for文の <= の部分に間違いがあり、 i=10 のときに配列 data の要素数を越えてしまっており、関数 subがそこに何かを代入してしまうことが事故の発生原因です。
ところがこのバグはこの for文のところでは異常が発生しないことがあるのです。C言語や他の言語についてもおおよそ言えることですが、コンパイラは配列の要素数と実際にアクセスされる範囲をチェックしません。
異常を発生するか否かは実行時のメモリーの配置がどうなっているのかが問題なのです。実行時のメモリーの配置が data の10個の要素の後に i の変数が配置されているのであればかなりの確率で for 文で異常を起こしますが、 違う変数やスタックなどが配置されているときにはプログラムのどこで異常を起こすかは予想ができません。
このプログラムをデバッグしているうちに下の例の様に変数 i の前にダミーの変数(例えば j)を偶然に定義し、実行すると正常に動作することもあります。
main()
{
int data[10];
int j;
int i;
:
:
for(i=0;i<=10;i++)sub(i,&data[i]);
:
:
}
「これでデバッグは完了!」としたら運がよければその後問題は発生しないかも知れません。がしかしこのプログラムを他のプログラムに移植したり、変更や機能追加をした
ときにまた動作の異常を起こす可能性があります。
実数値を使っているときにもいろいろと厄介なバグが発生してしまいます。
#include <stdio.h>
main()
{
float a;
a=1.0/10.0;
if(a==0.1)printf("Ok!\n");
}
という初歩的なプログラム。これを実行すると何も表示されません。1を10で割ると0.1のはずなのですが if 文は0.1だとは判断してくれないのです。じつは私も初心者の頃はこれが不思議でなりませんでした。実数値(不動小数点数)は正確に値を表現できることは希で殆どの場合、近似値でしかないと知るまでには時間がかかったものです。
上記のプログラムの動作を詳細に見てみると、
#include <stdio.h>
main()
{
float a; ← 単精度の実数変数 a を定義。
a=1.0/10.0; ← 倍精度の1.0を倍精度の10.0で除算し、
単精度に変換をした後、単精度の変数 a に代入。
if(a==0.1)printf("Ok!\n"); ← 単精度の変数 a の値を倍精度に変換し、倍精度
の値0.1と比較し、同一であれば printf関数
を呼び出す。
}
0.1という値は2進数で表現すると、
0.1
x 2
-----
0.2 --- 0
x 2
-----
0.4 --- 0
x 2
-----
0.8 --- 0
x 2
-----
0.6 --- 1
x 2
-----
0.2 --- 1
x 2
-----
0.4 --- 0
:
:
となり 0.00011001100110011... と無限に繰り返される値となります。倍精度のときでも単精度のときでも表現できるビット数に限りがありますので、正確な値を保持できません。近似値なのです。1.0/10.0はC言語では倍精度どうしの演算になりますので当然結果も倍精度となります。この倍精度の値をそのまま単精度の変数に代入はできませんので倍精度の値を一度単精度に変換を行ないます。このときに表現できるビット数が減りますので近似値の値の精度が低下してしまいます。それをまた倍精度の0.1と比較するために単精度から倍精度に変換するのですが、このときには切捨てられたビットが復活するのではないので比較する値は0.1とは違ったものとなってしまうのです。
それでは if 文の表現を
if(a==(float)0.1)printf("Ok!\n");
として単精度どうしの比較とすれば結果は真となりますが、この表現はあくまでも上記の例のような場合に限られますので、
if(a>=0.1-0.0001 && a<=0.1+0.0001)printf("Ok!\n");
と値の範囲内にあれば真とするように表現するのが一般的になります。
実数値を整数値に代入したときにもいろいろと問題を生じることがあります。要するに切捨てられる小数以下の値が問題になることがあるのです。
私が以前苦労した問題で、『座票値が0.01mmを単位として動作する機械に相対座標値でデータを渡す』というプログラムを作ったのですが、予期していた位置に機械が動いてくれずに誤差がどんどん増えてゆくという不具合になったことがあります。
いま10個データが次の様になっていたとします(簡単にするためにX座標のみを取り扱います)。
0.3175
0.635
0.9525
1.27
1.5875
1.905
2.2225
2.54
2.8575
3.175
この値が引数の配列 data に、個数が引数 ndata に入っているとして次の関数を考えてみます。
output(data,ndata)
float data[];
int ndata;
{
float prev;
int i,x;
prev=0.0;
for(i=0;i<ndata;i++){
x=(data[i]-prev)*100.0;
printf("%d\n",x);
prev=data[i];
}
}
この結果は、
31
31
31
31
31
31
31
となります。なんとなく良さそうな結果なのですが、実はこれでは機械が1/100mm以内の精度を保って移動してくれないのです。機械の方はこのデータを元にしてどのように移動するかといえば、
元データ 出力値 機械の移動先 誤差
0.3175 31 0.31 -0.0075
0.635 31 0.62 -0.015
0.9525 31 0.93 -0.0225
1.27 31 1.24 -0.03
1.5875 31 1.55 -0.0375
1.905 31 1.86 -0.045
2.2225 31 2.17 -0.0525
2.54 31 2.48 -0.06
2.8575 31 2.79 -0.0675
3.175 31 3.10 -0.075
となって誤差が膨らんでいってしまうのです。一体なにが間違いの元かといえば、相対値の考え方で、何を基準とした相対位置を出力しなければならないかを間違っているのです。上のプログラムの場合、データの前の位置からの相対位置を出力してしまっているのです。
本当は機械の現在の位置からの相対位置を出力すべきだったのです。そこで、
output(data,ndata)
float data[];
int ndata;
{
float prev;
int i,x;
prev=0.0;
for(i=0;i<ndata;i++){
x=(data[i]-prev)*100.0;
printf("%d\n",x);
prev+=(float)x/100.0;
}
}
と for ループ内の最後の式を prev=data[i] から prev+=(float)x/100.0 とすると
31
32
32
31
32
32
32
32
31
32
となり、誤差も
元データ 出力値 機械の移動先 誤差
0.3175 31 0.31 -0.0075
0.635 32 0.63 -0.005
0.9525 32 0.95 -0.0025
1.27 31 1.26 -0.01
1.5875 32 1.58 -0.075
1.905 32 1.90 -0.005
2.2225 32 2.22 -0.0025
2.54 32 2.54 -0.00
2.8575 31 2.85 -0.0075
3.175 32 3.17 -0.005
かなり少なくなっています。しかし、これでもまだ誤差が大きいのです。0.01mm単位の整数値で出力するのであれば、実際のデータとの誤差は±0.005未満に収束させることができるはずです。そこで、
output(data,ndata)
float data[];
int ndata;
{
float prev,delta;
int i,x;
prev=0.0;
for(i=0;i<ndata;i++){
delta=(data[i]-prev)*100.0;
if(delta>=0.0)x=delta+0.5;
else x=delta-0.5;
printf("%d\n",x);
prev+=(float)x/100.0;
}
}
と整数値に変換するときに四捨五入をするようにすると、
32
31
32
32
32
31
32
32
32
32
元データ 出力値 機械の移動先 誤差
0.3175 32 0.32 0.0025
0.635 31 0.63 -0.005
0.9525 32 0.95 -0.0025
1.27 32 1.27 0.0
1.5875 32 1.59 0.0025
1.905 31 1.9 -0.004999
2.2225 32 2.22 -0.0025
2.54 32 2.54 -0.0
2.8575 32 2.86 0.0025
3.175 32 3.18 0.004999
となって目標の精度でデータを出力することができるようになりました。
実際にプログラムを組んでみると驚かされる程に色々な種類のバグが発生します。上で紹介した例はほんの一部分です。プログラムの作り初めの頃(いわゆる初心者)は、バグが発生しないかどうかとビクビクするものですが、多くのバグを体験することが上達につながるものです。バグを取るのが上手か否かでその人の腕が分るといっても言い過ぎではないと思います。
それではまた次回としましょう。