あざらしとペンギンの問題

主に漫画、数値計算、幾何計算、TCS、一鰭旅、水族館、鰭脚類のことを書きます。

桁落ちを簡単に検出&表示

桁落ちは数値計算において精度を下げる重要な原因となります。
自分のテスト用ですが、C++ で桁落ちを検出して表示するプログラムを書きました。
小数に double 型を使用したC++ プログラムにおいて、次の mydouble.h をインクルードすると、桁落ちが生じるときに警告を出して入力と結果の仮数部の2進小数を表示します。元のソースコードをいじる必要は基本的にはありません。ただし、あくまで自分用に作ったのでコンパイルエラーが出ることがあります。そのときは自分で適当に直してください。

https://gist.github.com/azapen6/6111d084d47bc7c5d33c97f60bb84ebc

追記(2016/7/15): と思っていたら clang でコンパイルしたら printf に渡す MyDouble の変換でエラーが出たので、test_clang.cpp を追加しました。gcc では test.cpp も問題なくコンパイルできるはず。

もっとも、定義している演算子が少ない(例えば += などは書いていない)ためそんなものはないと怒らたり、オーバーロードしている演算子の引数の型が少ない(例えば unsigned int などは書いていない)ため、暗黙的な型変換で曖昧性が生じるなどのエラーは結構出ます。そのときは演算子を追加するなりして対処してください。

で、何やってるの?

具体的には、指数が同じで上位から precision で指定した桁数が一致した場合に動作します。例えば、precision = 23 とすると float 精度で値が得られないような桁落ちが生じた場合に警告を出して仮数部を表示します。

実行例を見せると、test.cpp

#include "mydouble.h"

int main() {
	double a = 1.0;
	double b = 0.5;
	double c = 1.0;
	double d = 0.0;
	double e = 1e-7;
	double f = a + e;
	double g = -f;
	double h = a + 2 * e;

	printf("%.15lf - %.15lf = %.15lf\n\n", a, b, a - b);
	printf("%.15lf - %.15lf = %.15lf\n\n", a, c, a - c);
	printf("%.15lf - %.15lf = %.15lf\n\n", d, e, d - e);
	printf("%.15lf - %.15lf = %.15lf\n\n", f, a, f - a);
	printf("%.15lf + %.15lf = %.15lf\n\n", a, g, a + g);
	printf("%.15lf - %.15lf = %.15lf\n\n", h, a, h - a);
	return 0;
}

に対する出力は

1.000000000000000 - 0.500000000000000 = 0.500000000000000

1.000000000000000 - 1.000000000000000 = 0.000000000000000

0.000000000000000 - 0.000000100000000 = -0.000000100000000

Loss of significance is detected in
1.000000100000000 - 1.000000000000000

+1 00000 00000 00000 00000 00011 01011 01011 11111 00101 00110 11
+1 00000 00000 00000 00000 00000 00000 00000 00000 00000 00000 00
+0 00000 00000 00000 00000 00011 01011 01011 11111 00101 00110 11

1.000000100000000 - 1.000000000000000 = 0.000000100000000

Loss of significance is detected in
1.000000000000000 + -1.000000100000000

+1 00000 00000 00000 00000 00000 00000 00000 00000 00000 00000 00
-1 00000 00000 00000 00000 00011 01011 01011 11111 00101 00110 11
-0 00000 00000 00000 00000 00011 01011 01011 11111 00101 00110 11

1.000000000000000 + -1.000000100000000 = -0.000000100000000

1.000000200000000 - 1.000000000000000 = 0.000000200000000

となります。

注目すべきは、最後の計算では前2つと違って何も出力されていないということです。よく見ると前2つの例は1の後小数第23位まで 0 で一致しているので当然検出されます。それに対して最後の例の入力と結果を2進表示すると

+1 00000 00000 00000 00000 00110 10110 10111 11110 01010 01101 01
+1 00000 00000 00000 00000 00000 00000 00000 00000 00000 00000 00
+0 00000 00000 00000 00000 00110 10110 10111 11110 01010 01101 01

となり、小数第23位が異なっているために検出されなかったのです。