2012年12月5日水曜日

丸め誤差の脅威( ゚ρ゚ )アゥー

みなさんはプログラムの数値がコンピュータ内では全て2進数として扱われていることはご存知ですよね!?
実はこの性質が引き起こすバグが存在し得ます。


我がラボメンバーの大半はfor文なんかのループカウンタや終了条件として必ずint型を使っている人が多いのではないかと思います。
それでいいんです!

以下ダメなパターンを見てみましょう。
#include<stdio.h>

int main()
{
  double d;
  for(d = 0.0; d <= 3; d += 0.1)
    {
      printf("d = %f\n", d);
    }
  
  return 0;
}
このソースコードの6行目のfor文の終了条件より、最終的に以下の出力が期待できます。
d = 3.000000
ところがどっこい、実際の出力の最後の部分は以下のようになります。
d = 2.500000
d = 2.600000
d = 2.700000
d = 2.800000
d = 2.900000
d=3.0まで行ってくれません(´・ω・`)ショボーン
何でかなー?

これは2進数特有の誤差によるものです。
コンピュータ内部では"0.1"という数字を丸っきり誤差なくして表現することができません。
ここでは小数点以下6桁までしか表示させていませんが、それよりも下の桁に"0.1"の丸め誤差が溜まっていってしまっているのです。
doubleではなくてfloatでやればもっと分かりやすい結果になるでしょう。
厳密な条件式に浮動小数点型を使うのは禁物です。

上のソースコードの一つの解決策としては次のようなものが考えられます。
#include<stdio.h>

int main()
{
  int    i;
  double d;
  for(i = 0; i <= 30; i++)
    {
      d = i/10.0;
      printf("d = %f\n", d);
    }
  
  return 0;
} 
これで無事に以下のような出力が得られます。
d = 2.500000
d = 2.600000
d = 2.700000
d = 2.800000
d = 2.900000
d = 3.000000

余談ですが、筆者はC++ユーザーですので、便利なIT++ライブラリを使うと次のような手法が使えます。
#include<cstdio>
#include<itpp/itbase.h>

int main()
{
  itpp::vec Value = "0:0.1:3"; 
  for(int i = 0; i < Value.size(); i++)
    {
      std::cout << Value[i] << std::endl;
    }
  
  return 0;
} 
特筆すべきは6行目の宣言ですが、ここでValueという名前の配列(のようなもの)を作っているのですが、初期値として0~3までを0.1刻みにした値を与えています
そしてfor文では配列の要素数だけループするようにしています。
C++ユーザーでなくても、配列をこういう風に初期化できる関数を作っておくと便利なのかもしれませんね!

0 件のコメント :

コメントを投稿