おひさしぶりになってしまいました。ポケコントレーナー @plageoj です。
上記の記事で新たなポケコントレーナーの誕生を垣間見たのですが(記事を参考にしていただきありがとうございます)、C言語の計算誤差にお困りのようだったので、改めて調べてみることにしました。
変数のサイズについて
現代環境
手元の elementary OS 8 + gcc 13.3.0 で次のようなプログラムを実行してみます。
#include <stdio.h>
int main(){
printf("char:\t%lu\tshort:\t%lu\tint:\t%lu\tlong:\t%lu\tfloat:\t%lu\tdouble:\t%lu\n", sizeof(char), sizeof(short), sizeof(int), sizeof(long), sizeof(float), sizeof(double));
printf("Ushort:\t%lu\tUint:\t%lu\tUlong:\t%lu\n", sizeof(unsigned short), sizeof(unsigned int), sizeof(unsigned long));
printf("Llong:\t%lu\tLdbl:\t%lu\n", sizeof(long long), sizeof(long double));
return 0;
}
このような出力が得られます。型ごとのサイズをバイト単位で表示したものです。
char: 1 short: 2 int: 4 long: 8 float: 4 double: 8
Ushort: 2 Uint: 4 Ulong: 8
Llong: 8 Ldbl: 16
参照:
PC-G850 環境
では同じプログラムをポケコンで実行します。long long
が使えなかったので、そこだけオミットします。
#define U unsigned
#define L long
main(){
printf("chr\t%d\tsrt\t%d\tint\t%d\n", sizeof(char), sizeof(short), sizeof(int));
printf("lon\t%d\tflt\t%d\tdbl\t%d\n", sizeof(L), sizeof(float), sizeof(double));
printf("Usr\t%d\tUin\t%d\tUln\t%d\n", sizeof(U short), sizeof(U int), sizeof(U L));
printf("Ldb\t%d\n", sizeof(L double));
}
コンパイルして実行すると、結果は次のようになります。
chr 1 srt 2 int 2
lon 4 flt 4 dbl 8
Usr 2 Uin 2 Uln 4
Ldb 8
*EXIT (80)
int
, long
, unsigned int
, unsigned long
, long double
の精度が半分しかないということになります。が、今回は本筋ではありません。
ここではとりあえず float
のサイズが 4B(32bit) であるということだけ覚えておけばOKです。
デバッグ
int main (void) {
float A, B;
printf("Input R1(Zero OK)");
scanf("%f", &A);
printf("Input R2(Zero NG)");
scanf("%f", &B);
printf("R=%lf", A * B / (A + B));
return 0;
}
引用:https://qiita.com/bockring/items/714e190eaec0b8655a5c
このプログラムを実行し、A
に 50
、B
に 1e6
を入力してみると、A+B
が 1000050
ではなく 1000000
(1e6) のままになっていることがわかりました(地道な print デバッグの結果です)。
A+B
の結果を一旦変数に格納してメモリ内容を見てみることにします。
main(){
float a=50, b=1e6;
float ans=a+b;
printf("%f\t%p\n", a, &a);
printf("%f\t%p\n", b, &b);
printf("%f\t%p\n", ans, &ans);
}
実行結果はメモリの状況によって異なりますが、このようになります。
50.000000 70A4
1000000.000000 70A0
1000000.000000 709C
*EXIT (70)
ここですかさず BASIC
キーを押して RUN モードに切り替え、さらに MON
コマンドを打って機械語モニタに切り替えます。
MACHINE LANGUAGE MONITOR
表示が出てプロンプトが *
に変わったら、D709C
(アドレスは適宜変更してください)と入力してメモリダンプを見ます。
メモリダンプはこのようになっていました。
709C : 00 60 10 00 (= 1e6)
70A0 : 00 60 10 00 (= 1e6)
70A4 : 00 10 50 00 (= 5e1)
これだけではよくわかりませんね。他にも色々な数値を入れて見てみると……
00 40 20 00 (= 2e4)
02 50 10 00 (= 1e25)
00 50 99 99 (= 99999 だが丸め誤差が発生)
00 48 12 34 (=-12345 だが丸め誤差が発生)
結論
というわけで、どうやら PC-G850VS の C 言語は float
を
0000 EEEE EEEE S000 FFFF FFFF FFFF FFFF (32 bits = 4B)
指数部_____ 符号 仮数部_______________
として持っているようです。指数部と仮数部は二進化十進数(BCD)です。
IEEE 754 だと思ったら大間違い。有効桁数が4桁しかありません! これで 1e6 + 50
が計算できなかったのです。
指数が大きく異なる数値の計算は情報落ちが発生するので注意が必要です。
BASIC が仮数部10桁の精度をもっているのとは大きな違いです。
double ではどうなるのか
double
型で同じように確認したところ、次のようになりました。
00 18 11 23 45 67 89 00 (=-11.2345678912 だが丸め誤差が発生)
同じように最初の2Bが指数部と符号になっており、続く5Bが仮数部、最後に 00
が入るようです。
有効桁数は10桁です。
元のプログラムを double
に書き換えて実行してみます。
main(){
double a,b;
scanf("%f,%f", &a, &b);
printf("%.9f", a*b/(a+b));
}
50,1e6
49.997501720
*EXIT (50)
と、小数第5位までは正しい値が出ます。
ところが BCD の扱いがおかしいのか、順番を入れ替えると値が変わります(?)
1e6,50
49.997498520
*EXIT (50)
もちろん BASIC ではそんなことは起こりません。
C言語機能は開発元が違うので、BASIC と処理が違うという可能性は十二分にあります。
C言語をコンパイルしたあとの内部表現に問題があるのか、除算処理に問題があるのかわかりませんが、それを調べるのはまた別の機会にします。