わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

0.1を10回足しても,1に届かない

いきなりですが問題です.次のプログラムは,何を出力するでしょうか.

#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を超えない」)

*1:バージョンは4.1.2,4.3.4,4.6.3.最後のは,Ubuntu 12.04です.

*2:トルエンディアンだから.

*3:「1」や「2」といった定数を,直接16進ダンプすることはできないので,いったん変数に入れている次第です.