概要
概要
gcc を用いて,より高精度の数値計算を行なう方法を紹介します.
ここで「高精度」というのは,科学技術計算における物理現象の予測精度とか大次元の連立一次方程式を高精度で解く方法とか精度保証などではなく,浮動小数点数の表現精度と演算精度のことです.
また,「総論」というのはここの一連のページの中の総論という意味です. 題材はあくまで私が実際に使っているものに限定しており,世の中にある諸々の alternative を統べるという意味ではありません.
対象は次のようなものです.
御存知のとおり,c などで使われている浮動小数点数の表現精度は有限です. i386 + gcc の環境では,浮動小数点数には単精度,倍精度,拡張倍精度,4 倍精度 [1] (float, double, long double [2], __float128) の 4 種類があり,各々の表現精度は 10 進数で約 7, 16, 19, 34 桁です. (5 節 を参照.)
ほとんどの場合には倍精度で事足りると思われますが,そうでないこともあり得ます. (そもそも,事足りている人はここを読んでいないでしょうし.) 倍精度以上の精度が必要になる事態としては,次のような場合が想定されます.
これらを解決するためには,定式化や計算法の改良,あるいは精度保証付き数値計算法を追求するのも良いでしょう. ただ,浮動小数点数の表現精度,演算精度を高めれば解決する場合もありますし,それで済めば楽なので,選択肢の一つとしてお勧めいたします.[6] 以下ではそのやり方を紹介します.
[1] 4 倍精度 (__float128) を利用できるのは gcc 4.6 以降からです.
(対象となる CPU は i386, x86_64, ia64, etc. です.)
厳密に言えば __float80 も使えるようになりましたが,情報が少なく long double との差異も分からないので,言及しません.
[2] 環境を i386 + gcc に限定しない場合,long double は必ずしも同じものを指していないので要注意です.
例えば SPARK + gcc では long double はサイズが 128 bit ,表現精度は約 34 桁です.
(これは「拡張倍精度」でなく「4 倍精度」と呼ばれています.)
[3] これらの場合には定式化を再考するべきでしょう. そうするとしても,新旧の定式化の結果が比較できれば検証が楽です.
[4]「近似」とは,「粗雑」なことではなく精度を任意に制御できる状態である.
(by Van Dyke)
適切に精度を評価していないような「近似」計算は時として危険です.
[5] その無限級数の収束性を高めるために Euler 変換や Shunks 変換などを使うにしても,その変換自体の正当性を確認するために,結局は精度の良い計算結果が必要です.
[6] これを書いた動機は,FreeBSD/i386 + gcc という環境下にて表現精度に関係した良い資料が見つけられなかったことです.
高精度化のために拡張倍精度 (long double) を使う長所としては,以下のことが挙げられます.
また短所としては,以下のことが挙げられます.
拡張倍精度は,次のようにして利用することができます.
#include <stdio.h> #ifdef __FreeBSD__ #include <floatingpoint.h> /* 必ず include する. */ long double sinl(long double); /* sinl() を prototype 宣言する. */ #endif int main(void) { long double x,y; /* 型は long double である.*/ #ifdef __FreeBSD__ fpsetprec(FP_PE); /* これを演算の実行文の前におく. */ #endif x = 0.25L*3.14159265358979323846L; /* 定数はこのようにする. */ y = sinl(x); /* sinl() は long double 用の sin() */ printf("sin(pi/4) = %Le\n",y); /* 入出力の書式は L を付加する. */ return 0; }
一見簡単そうですが,細かい注意事項がいくつかあります. 詳細は long double をご覧下さい.
高精度化のために 4 倍精度 (__float128) を使う長所としては,以下のことが挙げられます.
また短所としては,以下のことが挙げられます.
4 倍精度は,次のようにして利用することができます.
#include <stdio.h> #include <quadmath.h> /* 必ず include する. */ int main(void) { char c[80]; __float128 x,y; /* 型は __float128 である.*/ x = 0.25Q*3.1415926535897932384626433832795029Q; /* 定数はこのように */ y = sinq(x); /* sinq() は __float128 用の sin() */ quadmath_snprintf(c,80,"%.40Qf",y); /* 入出力の書式は Q を付加する. */ printf("sin(pi/4) = %s\n",c); return 0; }
一見簡単そうですが,細かい注意事項がいくつかあります. 詳細は __float128 をご覧下さい.
i386 にて long double 以上の精度で計算をしたい場合,より高精度な浮動小数点数を定義し,その演算をソフトウェアにてエミュレートするしか方策はありません. 幸いなことに,Cephes からそれを実現するための library が配布されています.
高精度化のために qlib を使う長所としては,以下のことが挙げられます.
また短所としては,以下のことが挙げられます.
この library は c および c++ から利用することができます.
c++ 言語から使う場合,演算子オーバーローディングの機能が使えるので,四則演算も含めて普通の c 言語のように書けます.
#include <stdio.h> #include "qfloat.h" // 必ず include する. extern qfloat qpi; // M_PI のような定数は変数として用意されている. int main(void) { qfloat x,y; // 型は qfloat である. x = 0.25*qpi; y = sin(x); // sin() は qlib の qsin() が使われる. qprint(30,"sin(pi/4) =",y,"\n"); // 数値を出力する. return 0; }
この library の作成や利用法の詳細は qlib をご覧下さい.
浮動小数点数 x の値は,符号 (sign) s,基数 (radix) b,指数 (exponential) e,仮数 (mantissa) p から次式で計算します.
x = s*pow(b,e)*p
仮数 p の値域は 1 <= p < b です. (符号が正であるとして,x の値を増していけば p の値も増しますが,p = b に達したら,e が 1 増え p は 1 に戻されます.)
基数は固定値 (b = 2) であり,それは浮動小数点数の中には記録されません. 従って,浮動小数点数には,符号,指数,仮数の三つが記録されます. ただし,その記録は上式とは違う形でされています. すなわち,
浮動小数点数の種々の型とサイズ,精度は次表の通りです. (下の二つは参考まで.)
変数の型 | サイズ (bit) | 符号 (bit) |
指数部 (bit) | 仮数部 (bit) | 未使用 (bit) | 10 進数の精度 (桁) |
---|---|---|---|---|---|---|
float (i386) | 32 | 1 | 8 | 23 | 0 | 7 |
double (i386) | 64 | 1 | 11 | 52 | 0 | 16 |
long double (i386) | 96 or 128 | 1 | 15 | 64 | 16 or 48 | 19 |
__float128 (i386) | 128 | 1 | 15 | 112 | 0 | 34 |
long double (SPARK) | 128 | 1 | 15 | 112 | 0 | 34 |
qfloat (qlib) | 448 | 32 | 32 | 352 | 32 | 106 |
ここで,10 進数の精度は次のように計算しました.
(p の bit 数) x log10(2) (23+1) x log10(2) = 7.224 (52+1) x log10(2) = 15.955 64 x log10(2) = 19.266 (112+1) x log10(2) = 34.016 352 x log10(2) = 105.962
float, double, __float128 および SPARK の long double の p の bit 数に 1 を加えているのは implicit bit の分です.
高精度の浮動小数点数を実現する library は qlib の他にもいくつかあります. FreeBSD の ports になっているものは ports/math/ の下にあります. (例えば ccmath 等.) 今後これらを使う機会があれば,ここで紹介します.
選択にあたっては,用途はもちろんですが,次のような点に着目すれば良いでしょう.