Entry

プログラミングメモ - 「浮動小数点数は常に等号で条件判断しちゃいけない」は誤り

2009年04月04日

C/C++ (あるいは他の言語も)の常識として,浮動小数点数型の変数を素のまま等号で結んで条件判断しちゃいけないってのがあります。

例えばこういうもの。座標 p1(1.0, 0.0) と p2(0.0, 1.0) の点を,それぞれ左上原点中心・時計回りに45.0度/-45.0度回転するプログラムです。

#include <iostream>
#include <cmath>

static const double PI = 3.14159265;

struct Point {
  double x_;
  double y_;
  bool operator==(const Point& other) const {
    return x_ == other.x_ && y_ == other.y_;
  };
};

bool
rotate(Point& p, double rad) {
  double x = p.x_ * cos(rad) - p.y_ * sin(rad);
  double y = p.x_ * sin(rad) + p.y_ * cos(rad);
  p.x_ = x;
  p.y_ = y;
  return true;
}

int
main(int argc, char* argv[]) {
  Point p1 = {1.0, 0.0};
  Point p2 = {0.0, 1.0};
  const double rad1 =  45.0 * PI / 180.0;
  const double rad2 = -45.0 * PI / 180.0;

  rotate(p1, rad1);
  rotate(p2, rad2);

  if (p1 == p2) {
    std::cout << "same." << std::endl;
  } else {
    std::cout << "different." << std::endl;
  }
#if 0
  std::cout.precision(16);
#endif
  std::cout << "p1 (" << p1.x_ << ", " << p1.y_ << ")" << std::endl;
  std::cout << "p2 (" << p2.x_ << ", " << p2.y_ << ")" << std::endl;

  return 0;
}

p1 と p2 を指定した角度で回転させると,手計算では同じ座標になるはず。しかし,結果はこうなります。

% rotate
different.
p1 (0.707107, 0.707107)
p2 (0.707107, 0.707107)

結果は同じように見えるのに,different と表示されました。奇妙ですね。

もうお分かりでしょうけど,浮動小数点数の計算には,必ずと言っていいほど誤差が入るので,同じ値になることはほとんどありません。同じ値になるときは,同じリテラルを元にして,同じ計算をしたときくらいだと思ったほうがいい。上のプログラムでは,定数 PI や弧度法からラジアンに変換する時の計算,それに sin(3) や cos(3) の返値に誤差が入り込みます。

誤差がどのように入るのか,std::cout の精度を上げて表示してみましょう。上のソースの #if を外した結果は,以下のようになります。

% rotate
different.
p1 (0.7071067818211393, 0.7071067805519557)
p2 (0.7071067805519557, 0.7071067818211393)

これで見ると,たしかに値が異なっています。冒頭で「浮動小数点数型の変数を素のまま等号で結んで条件判断しちゃいけない」と書いたのは,要するにこういうことで,こういう条件を設定すると,誤差によって成否が変わる不安定な条件になってしまうわけです。

で,ここからが本題。こういう話はある意味常識として,大抵のプログラマは認識している(はずな)んですけれど,どういうわけだか話が敷衍されて,変な方向に向かうことがあるみたい。例えば,

浮動小数点数型を等号で結んで条件判定することは常に悪。そんなことするやつは,腹切って死ね!

みたいな……。いや,死ねとゆのは演出だけど。

しかし,冒頭にある「素のまま」という留保は,「計算結果を正規化しないまま」ということなんですけど,正規化した結果,誤差を含まない形で等号が成立するのであれば,当然使用していいはずです。例えば,アプリケーションの仕様として,小数点第二位で四捨五入した結果をデータとして持つのであれば,普通に等号を使える。

浮動小数点数の値を持つ場合として,例えば,CAD の図面を扱うプログラムなんかがあります。図形を回転させたり拡大/縮小させたりすると,どうしても誤差が出る。しかし,ここで出てしまう誤差は,プログラム内部の有効桁数で正規化されるのが普通です。値を丸めたり,切り捨てたりしてる。で,こういうアプリケーションが吐き出したデータを扱う補助プログラムも,通常は親プログラムが吐き出すデータの有効桁数を考慮に入れて実装するはずです。

つまるところ,「浮動小数点数だから」という理由だけでもって等号の条件判断をしちゃいけないってのは,誤りだということです。

どういうわけだか,変な「覚え方」をしてた人がいて,残業するはめになってしまったので書いといた。こんなの覚えるもんでもなかろうに……。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN