Mae向きなブログ

Mae向きな日記のブログ版。ようやくこちらに移行してきました。

浮動小数点数の誤差問題

どんな問題が潜んでいるのか?

この問題の空欄を補充したものが以下になります。

taxi.c

これを実行すると、

$ ./a.out
走行距離[km]は2.1
金額は800円です。
$ ./a.out
走行距離[km]は2.0
金額は710円です。
$ ./a.out
走行距離[km]は2.285
金額は890円です。

のようになります。

このタクシーに2.1km、2.0km乗った時には問題ないのですが、2.285km乗った時に、本来は800円の料金のはずなのですが、890円の請求となってしまいます。これは、実数をコンピュータ内で2進数で表現するのに限界があるためで丸め誤差と呼ばれています。

誤差があるからしょうがないという結論で終わってもいいのですが、実際にコンピュータ内でどのように表現されているのか調べてみました。

0.285(10進法)をIEEE 754 単精度で表現してみよう

参考サイトの例の手順に習って求めてみたいと思います。

まず、0.285を2進数に変換すると、以下のようになります。手計算でやってもよかったのですが、面倒なので「小数点つき10進数を2進数に変換する方法とツール - 具体例で学ぶ数学」を利用しました。

0.0100\ 1000\ 1111\ 0101\ 1100\ 0010\ 1000\ 1111\ 0101\ 1100\ 0010\ 1000\ 1111\ 01

小数点を左に移動させ、1だけを左に残すと、

1.0010\ 0011\ 1101\ 0111\ 0000\ 1010\ 0011\ 1101\ 0111\ 0000\ 1010\ 0011\ 1101 \times 2^{-2}

となります。

  • 正の数なので、符号は0
  • fractionは小数点の右側だけであり、23ビット分なので0010 0011 1101 0111 0000 101
  • 指数は-2であるが、バイアスを加える必要があり、-2+127=125。2進数に変換すると、01111101

この結果をまとめると、以下のようになり、コンピュータの中では

0011\ 1110\ 1001\ 0001\ 1110\ 1011\ 1000\ 0101

と表現されていることになります。fraction部を求めるときに、23ビット分の情報に丸め込んでしまってますので、ここで誤差が生じるのは明らかですね。

プログラムで何が起こったのか?

少し遠回りをしましたが、タクシー料金を求めるプログラムを実行したときに、実際、何が起こったのかをみてみたいと思います。参考サイトに共用体を利用して浮動小数点数の内部表現を取得する方法が紹介されていましたので以下のようなプログラムを作り、変数ryoukinの値の変化を追ってみました。

taxi_trace.c

実行結果

*** scanfで2.285が入力された。
2.285000 ( 40123D71 )
01000000000100100011110101110001
符号部 : 0
指数部 : 80
仮数部 : 123D71

*** 12行目実行直後
0.285000 ( 3E91EB88 )
00111110100100011110101110001000
符号部 : 0
指数部 : 7D
仮数部 : 11EB88

*** whileループ内:15行目実行直後
*** ここでkyoriが0になると思ってしまう!
0.000000 ( 33B851EC )
00110011101110000101000111101100
符号部 : 0
指数部 : 67
仮数部 : 3851EC

*** whileループ内:15行目実行直後
-0.285000 ( BE91EB82 )
10111110100100011110101110000010
符号部 : 1
指数部 : 7D
仮数部 : 11EB82

解決策

その1

タクシー料金産出の元となる距離をkmではなくm単位で扱う。

その2

計算機イプシロンを利用して以下のように変更する。

その3

何かありましたら、コメント欄にお願いします。

参考サイト

おまけ1

0.285(10進法)をIEEE 754 倍精度で表現してみました。

double_print.c

0.285000 ( 3FD23D70A3D70A3D )
0011111111010010001111010111000010100011110101110000101000111101
符号部 : 0
指数部 : 3FD
仮数部 : 23D70A3D70A3D

0.285は先に求めたように、 1.0010\ 0011\ 1101\ 0111\ 0000\ 1010\ 0011\ 1101\ 0111\ 0000\ 1010\ 0011\ 1101 \times 2^{-2} ですので、

  • 符号は0
  • fractionは52ビット分なので0010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101
  • 指数は-2であるが、バイアスを加えて -2+1023=1021。2進数で01111111101

この結果をまとめると、以下となり実行結果と同じになります。

0011\ 1111\ 1101\ 0010\ 0011\ 1101\ 0111\ 0000\ 1010\ 0011\ 1101\ 0111\ 0000\ 1010\ 0011\ 1101

おまけ2