コンピューターサイエンスの初学者です。課題で小数を扱う機会があり、気になったので2進数の中身を出力してみたら、入れたはずの値と違ったので理由を調べてみました。
記載している内容に間違いがありましたら教えていただけますと幸いです。
浮動小数点数・固定小数点の定義については扱いませんので、知りたい方はこちらの記事がおすすめです。
浮動小数点数の中身を表示してみる
こちらの記事を参考に、floatの値の中身を表示してみました。
void outputBit(std::string txt, float val){
std::cout << txt << "(" << val << "): ";
union { float f; int i; } a;
a.f = val;
for(int i = 31; i >= 0; i-- ){
std::cout << ((a.i >> i) & 1);
if(i == 31 || i == 23)
std::cout << "-";
}
std::cout << std::endl;
}
int main( void ) {
float value = -42.42f;
outputBit("Value", value);
return 0;
}
このビット列が表している値を 10進数で表すと、次の値になります。

符号部(先頭1ビット)
値がマイナスの場合1、プラスの場合0が入っています。今回は0です。
指数部(真ん中8ビット)
もとの値を2進数にして、整数の部分が1になるように小数点を動かして調整したとき(数字が「1.XXX」の形になるように調整したとき)、小数点をいくつずらせばいいかが格納されています。小数点は左右のどちらにでも動く可能性があるため、127を中心として固定した値が入っています。詳しくはこちらをご参照ください。
今回は132-127=5が入っていることになります。2⁵は32です。
仮数部(末尾23ビット)
指数部で小数点を動かした後の数字が格納されています。仮数部の先頭はかならず1になるので、省略されています。
省略された1を追加すると、それぞれの2進数での重みは以下のようになります。
ここから、1が立っている位置だけ抜き出してみます。
| ビット位置 | 2進での重み | 分数表現 | 10進値 |
|---|---|---|---|
| 暗黙の1 | 2⁰ | 1 | 1.0 |
| 2 | 2⁻² | 1/4 | 0.25 |
| 4 | 2⁻⁴ | 1/16 | 0.0625 |
| 7 | 2⁻⁷ | 1/128 | 0.0078125 |
| 8 | 2⁻⁸ | 1/256 | 0.00390625 |
| 10 | 2⁻¹⁰ | 1/1024 | 0.000976562... |
| 12 | 2⁻¹² | 1/4096 | 0.000244140... |
| 13 | 2⁻¹³ | 1/8192 | 0.000122070... |
| 14 | 2⁻¹⁴ | 1/16384 | 0.000061035... |
| 19 | 2⁻¹⁹ | 1/524288 | 0.000001907... |
| 21 | 2⁻²¹ | 1/2097152 | 0.000000476... |
| 合計 | 1.325624465... |
これらをすべて足し合わせると、約1.32562となります。これが正規化された仮数の値になります。
こちらに指数部の2⁵(32)をかけ合わせると、
$1.32562 × 32 = 42.41984$
と、42.42に近似した値が格納されていることが分かります。
このように、浮動小数点数は多くの小数を近似値でしか表せないようなのです。
浮動小数点数を固定小数点に変換してみる
今回は、int型の変数をビット列を保存するための入れ物とみなして、先頭1ビットに符号、真ん中23ビットに小数点以上、末尾8ビットに小数点以下の値を格納することにします。
自作固定小数点数の中身を表示する関数を作ってみました。
void outputBit(std::string txt, int val){
std::cout << txt << "(" << val << "): ";
for(int i = 31; i >= 0; i-- ){
std::cout << ((val >> i) & 1);
if(i == 31){
std::cout << "-";
} else if(i == 8){
std::cout << "-";
}
}
std::cout << std::endl;
}
固定小数点への変換方法は簡易的に、「変換対象の数字に2⁸をかける」という方法をとってみます。逆に浮動小数点数への変換方法は「変換対象の数字を2⁸で割る」こととします。
実行結果は以下のようになりました。

あれ、もとの値42.42から42.418に変わってしまいました。
何が起きているのか整理してみましょう。
先ほどの浮動小数点数の仮数部の小数点の位置を正しい位置に戻した時の値はこうです。この左側の指数部の中身分の6bit+暗黙の先頭1bit(6bit)は整数の値、それ以降は小数の値を指しています。

浮動小数点を固定小数点に変換する際は「変換対象の数字に2⁸をかけ」ますので、小数点位置が8個右にずれます。

今回作成する固定小数点の小数部は8bitときまっていますから、この小数の値のうち8bit以外はバッサリ切り捨てられてしまいます。もとの値から固定小数点数へ変換したときに誤差が発生したのは、この無限に続く2進数の小数を、小数部8bitにおさめたためです。

上記の実行時結果のログで出力された固定小数点数の中身は浮動小数点数の小数点を8bit右にシフトしたのと等価の値が設定されていることがわかります。(値がマイナスの場合、intは2の補数表現になるため、この通りではありません。実際は10進数で計算したあとintに格納しているだけです。)

これは10進数の状態でも再現することができます。
もとの値42.42に2⁵(=256)をかけると小数が発生します。
$42.42 × 256 = 10859.52 $
int型の変数には小数値は入りませんので、小数の切り捨てが起きて10859が入ります。その値を復元しようとすると以下の値になるわけです。
$10859 ÷ 256 = 42.417969$
実行ログで出力された復元値42.418はこれが丸められた値になりそうです。
浮動小数点数・固定小数点数のどちらも、2進数で小数点以下の値を表現しているためbit数の範囲で表せない部分は近似するしかないのです。
この誤差に簡単に対応するとしたら、固定小数点数への変換の際に四捨五入する、などの対策があるでしょうか。
調べてみた感想
小数点は数学の話が絡んでくるのでずっと苦手意識があったのですが、調べてみて沼にハマってしまいました。銀行システムなどでfloatやdoubleを使うには注意が必要なのはこのためなんですね。そういった誤差に厳しいシステムを日々メンテナンスしてくださっている方々には感謝の念に堪えません。
私も日々コンピューターについて学んで精進していこうと心の奥底で誓ったのでした...!!

