Round off error festival on Japan, 2005. Sorry Japanese Only.

誤差フェスティバル

計算機に誤差はつきもの。というアレで、ここでは理論ではなく、実践で体験してみます。余裕があれば、その対処法も。
WindowsXP Pro SP2 : Visual C++ 7.1 および、FedoraCore4 : GCC 4.0.0 で検証しています

切り捨て誤差/四捨五入誤差

単純に、小数点以下の桁を切り捨てたり、四捨五入することで発生する誤差です。次のサンプルを見てください。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double d = 1.064453125;
 6 :   int    i = (int)d;
 7 : 
 8 :   printf("d = %10.8f\n", d);
 9 :   printf("i = %-10d\n", i);
10 : 
11 :   return 0;
12 : }
13 : 

あ、一応9行目「printf("i = %-10d\n", i);」は、全体10桁を左詰めで表示させます。たまにしか使いませんが。頭に「-」をつけると左詰です。

実行結果、まずFedoraCore4 : GCC 4.0.0。printfでの表示の際も、intへのキャスト時も切り捨てが行われるようです。

d = 1.06445312
i = 1

実行結果、次にWindowsXP Pro SP2 : Visual C++ 7.1。こちらは、printfでの表示の際には四捨五入、intへのキャスト時には切り捨てが行われるようです。

d = 1.06445313
i = 1

まとめ。切り捨てされるにせよ、四捨五入されるにせよ、それがプログラムの仕様であれば問題ありませんが、環境によって切り捨てられるのか四捨五入されるのかが異なるため、プログラムの仕様であることは望ましくないでしょう(どっちやねん)。ただ、キャスト時に四捨五入されるような環境があるとは思えませんし、それは大丈夫でしょうが、たとえば講義や試験などで「printfの表示は、四捨五入されます」なんて教えることは、誤解を招きます(私の通った大学がそうでした)。

キャスト問題

キャスト自体にも誤差が起こりえます。次のプログラムの実行結果を予測してみてください。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double     d;
 6 :   long long ll;
 7 :   
 8 :   d = 1.0 / 3.0;
 9 :   ll = (long long)( d * 24 * 60 * 60 );
10 : 
11 :   printf("\"   double\" size : %d\n", sizeof(double) );
12 :   printf("\"long long\" size : %d\n", sizeof(long long) );
13 :   printf(" d = %20.18f\n", d);
14 :   printf("ll = %-20d\n",  ll);
15 : 
16 :   return 0;
17 : }
18 : 

これ、仕事で実際に出てきた問題です。元のプログラムのどんなところで使われていたかを多少説明しますと。(唐突に)MS-Excelは日付と時刻をdouble値で表します。このプログラムは逆にdouble値から時刻を求めるものでした。あ、さらにいうとdouble値「1.0」は「24時00分00秒」を表します。「0.5」は「12時00分00秒」を表します。「0.25」は「06時00分00秒」、「0.125」は「03時00分00秒」、「0.0625」は「01時30分00秒」・・・、になります。これを簡単に求めるために小数部を(24*60*60)倍して秒数(long long値)に直しています(先に整数部分を切り捨てています)。もちろん、多少の切り捨て誤差が発生します。そこから時刻を求めるプログラムでした。(余談長すぎ)

さて、気になる実行結果。FedoraCore4 : GCC 4.0.0

"   double" size : 8
"long long" size : 8
d = 0.333333333333333315
ll = 28799

実行結果、次にWindowsXP Pro SP2 : Visual C++ 7.1

"   double" size : 8
"long long" size : 8
d = 0.333333333333333310
ll = 28800

さて、dの値は3分の1日を表しています(もちろん僅かな誤差を含んでいますが)。3分の1日というと、つまり8時間です。8時間を秒数に直してみると8*60*60=28800秒です。つまり、今回のプログラムではFedoraCore4 : GCC 4.0.0の方だけ誤差が出てしまっている、という結果になるわけです。しかし、この誤差はプログラムに手直しを加えることで修正可能です。あ、ちなみにバージョンはわからないですがRedHat、FreeBSD、SoralisでもFedoraCore4と同様の誤差が発生します。

修正は、以下のように行います。変更したのは9行目10行目のみです。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double     d;
 6 :   long long ll;
 7 :   
 8 :   d = 1.0 / 3.0;
 9 :   d *=  24 * 60 * 60;
10 :   ll = (long long)( d );
11 : 
12 :   printf("\"   double\" size : %d\n", sizeof(double) );
13 :   printf("\"long long\" size : %d\n", sizeof(long long) );
14 :   printf(" d = %20.18f\n", d);
15 :   printf("ll = %-20d\n",  ll);
16 : 
17 :   return 0;
18 : }
19 : 

はじめ私が誤差が出て困っているときは、long long型を使っていませんでした。そこで、まず型を広げることにしましたが、効果はありませんでした。ということは、問題は、型の大きさでないということです。また、24*60*60の部分を小数点で書いてみたり、86400にしてみたりしてみましたが、こちらも効果はありませんでした。

結局、原因はよくわかっていないのですが、さっき書いた、WindowsとFedoraの「切り捨て、四捨五入の違い」なんじゃないかと考えました。Windowsでは浮動小数点を整数にキャストする際には確かに小数点以下が切り捨てされるのですが、浮動小数点を何十倍かすると同時にキャストする場合は、四捨五入しているのではないのか、と考えています。ま、憶測ですけど。

そんなこんなで、先に浮動小数点の状態で秒数に直します(9行目)。そうすることで、なんと両環境ともにdの値は「d = 28800.000000000000000000」になります(つまり誤差がなくなっている)。本当は、28799.999999999999999999という値が格納されているのか、というとそうでもなさそうです。なぜって、Fedoraでは、printf時にも小数点以下切り捨てのハズですから。じゃあ、どうなっているのか、それは私も知りません(オイッ!)。申し訳ないっす。

結局私はよくわかっていない。ま、そのうち続きを書けたらなぁ。

Comments are welcome! We can be reached at whoinside_reshia@hotmail.co.jp
2005/07/04 23:06:27 UTC