いきなりですが問題です.次のプログラムは,何を出力するでしょうか.
#include <stdio.h> int main(void) { double i, j; for (i = 0, j = 1; i < j; i += 0.1) { printf("%g ", i); } printf("\n"); for (i = 0, j = 1; i <= j; i += 0.1) { printf("%g ", i); } printf("\n"); for (i = 1, j = 2; i < j; i += 0.1) { printf("%g ", i); } printf("\n"); for (i = 1, j = 2; i <= j; i += 0.1) { printf("%g ", i); } printf("\n"); return 0; }
「<」と「<=」の違いから,期待したい出力は
0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2
です.しかし手近で利用可能ないくつかのGCC*1でコンパイルし,実行したところ,どれも
0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9
となりました.この結果は,
- 0.1を10回足しても,1に届かない
けれども
- 1から始まって,0.1を10回足すと,2を超える
ように見えます.
ここから引き出される教訓は,「浮動小数点型の変数をループカウンタにしてはいけない」なのですが,もう少し調べてみます.
double型の変数の値,より正確にはメモリの状態を,バイト単位で出力してみます.
#include <stdio.h> void print_double(double d) { int i; printf("%4g: ", d); for (i = 0; i < sizeof(double); i++) { printf("%02x ", *((unsigned char *)&d + sizeof(double) - 1 - i)); } printf("\n"); } int main(void) { double i, j; for (i = 0, j = 1; i < j; i += 0.1) { print_double(i); } printf("----\n"); print_double(i); print_double(j); printf("\n"); for (i = 1, j = 2; i < j; i += 0.1) { print_double(i); } printf("----\n"); print_double(i); print_double(j); printf("\n"); return 0; }
関数print_double内のforループは,変数dを,格納されている番地があとのもの*2から順に取り出して,16進2桁で出力するというものです.
「----」より前は,0.1を足されていく変数iのメモリ状況,後ろは,forループを抜けたところでの変数iとj*3のメモリ状況を出力します.
出力は次のとおり.
0: 00 00 00 00 00 00 00 00 0.1: 3f b9 99 99 99 99 99 9a 0.2: 3f c9 99 99 99 99 99 9a 0.3: 3f d3 33 33 33 33 33 34 0.4: 3f d9 99 99 99 99 99 9a 0.5: 3f e0 00 00 00 00 00 00 0.6: 3f e3 33 33 33 33 33 33 0.7: 3f e6 66 66 66 66 66 66 0.8: 3f e9 99 99 99 99 99 99 0.9: 3f ec cc cc cc cc cc cc 1: 3f ef ff ff ff ff ff ff ---- 1.1: 3f f1 99 99 99 99 99 99 1: 3f f0 00 00 00 00 00 00 1: 3f f0 00 00 00 00 00 00 1.1: 3f f1 99 99 99 99 99 9a 1.2: 3f f3 33 33 33 33 33 34 1.3: 3f f4 cc cc cc cc cc ce 1.4: 3f f6 66 66 66 66 66 68 1.5: 3f f8 00 00 00 00 00 02 1.6: 3f f9 99 99 99 99 99 9c 1.7: 3f fb 33 33 33 33 33 36 1.8: 3f fc cc cc cc cc cc d0 1.9: 3f fe 66 66 66 66 66 6a ---- 2: 40 00 00 00 00 00 00 02 2: 40 00 00 00 00 00 00 00
倍精度で,符号1ビット,指数部11ビット,仮数部52ビットと考えて問題ないでしょう.そして,この出力から,次の2つを確認することができました.
- 0.1を10回足したときは「3f ef ff ff ff ff ff ff」で,「3f f0 00 00 00 00 00 00」で表される真の1よりも小さい.
- 1から始まって,0.1を10回足したときは「40 00 00 00 00 00 00 02」で,「40 00 00 00 00 00 00 00」で表される真の2よりも大きい.
現象には,ぎょっとさせられますが,話としては「丸め誤差」に起因する,よく知られたものだと思います.実行環境に依存する話である,言い換えるとCの仕様として常にこうなるというわけではないことも,付記しておきます.
(最終更新日時:Mon May 14 12:36:00 2012ごろ.当初のタイトルは「0.1を10回足しても1を超えない」)