PC-G850VS のC言語、float型の精度について

おひさしぶりになってしまいました。ポケコントレーナー @plageoj です。

https://qiita.com/bockring/items/714e190eaec0b8655a5c

上記の記事で新たなポケコントレーナーの誕生を垣間見たのですが（記事を参考にしていただきありがとうございます）、C言語の計算誤差にお困りのようだったので、改めて調べてみることにしました。

## 変数のサイズについて

### 現代環境

手元の elementary OS 8 + gcc 13.3.0 で次のようなプログラムを実行してみます。

```c
#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;
}
```

このような出力が得られます。型ごとのサイズをバイト単位で表示したものです。

```bash
char:	1	short:	2	int:	4	long:	8	float:	4	double:	8
Ushort:	2	Uint:	4	Ulong:	8
Llong:	8	Ldbl:	16
```

参照：

https://qiita.com/manontroppo1974/items/8ab6d1d318d0153903dc

### PC-G850 環境

では同じプログラムをポケコンで実行します。`long long` が使えなかったので、そこだけオミットします。

```c
#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です。

## デバッグ

```c:抵抗値計算のプログラム
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` の結果を一旦変数に格納してメモリ内容を見てみることにします。

```c
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` に書き換えて実行してみます。

```c
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言語をコンパイルしたあとの内部表現に問題があるのか、除算処理に問題があるのかわかりませんが、それを調べるのはまた別の機会にします。