とある検定試験の問題です。「この問題にはある問題が潜んでいます。その問題とは何ですか?」という問題にしてみてはどうかと思い立ちました。 pic.twitter.com/UVNLPbrlHm
— Ⓜⓐⓢⓐⓗⓘⓓⓔ Ⓜⓐⓔⓗⓐⓡⓐ (@maehrm) 2017年12月9日
どんな問題が潜んでいるのか?
この問題の空欄を補充したものが以下になります。
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進数に変換する方法とツール - 具体例で学ぶ数学」を利用しました。
小数点を左に移動させ、1だけを左に残すと、
となります。
- 正の数なので、符号は0
- fractionは小数点の右側だけであり、23ビット分なので0010 0011 1101 0111 0000 101
- 指数は-2であるが、バイアスを加える必要があり、-2+127=125。2進数に変換すると、01111101
この結果をまとめると、以下のようになり、コンピュータの中では
と表現されていることになります。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
何かありましたら、コメント欄にお願いします。
参考サイト
- IEEE 754 - Wikipedia
- 小数点つき10進数を2進数に変換する方法とツール - 具体例で学ぶ数学
- 浮動小数点数の内部表現を取得してみよう - Qiita
- 計算機イプシロン - Wikipedia
おまけ1
0.285(10進法)をIEEE 754 倍精度で表現してみました。
double_print.c
0.285000 ( 3FD23D70A3D70A3D ) 0011111111010010001111010111000010100011110101110000101000111101 符号部 : 0 指数部 : 3FD 仮数部 : 23D70A3D70A3D
0.285は先に求めたように、 ですので、
- 符号は0
- fractionは52ビット分なので0010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101
- 指数は-2であるが、バイアスを加えて -2+1023=1021。2進数で01111111101
この結果をまとめると、以下となり実行結果と同じになります。
おまけ2
浮動小数点がどんな風に扱われているかですが、アセンブリコードに変換すると何か見えるんじゃないかと思ってやってみました。案の定、赤点線枠のところがそうですね。得られた2進数を10進数で表現すると1049750405になります。その右側 ## はコメントでしょうか。すでに0.285じゃないところが面白い! pic.twitter.com/I9tFgOFhCv
— Ⓜⓐⓢⓐⓗⓘⓓⓔ Ⓜⓐⓔⓗⓐⓡⓐ (@maehrm) 2017年12月18日