Page List

Search on the blog

2010年9月22日水曜日

多倍長演算 --doubleの精度!?

今日は、ついに多倍長演算デビュー。
PKU4問解いてやった。C++で自力で実装しようと思ってたが結局Javaでやることにした。信頼性の高い既成品があれば、当然そちらを使うべき。(あー、いつの間にかビジネスチックな思考になってしまった。。)

Javaは2種類の多倍長演算に対応する型を装備している。
まずは、BigInteger。その名の通り大きな整数。これはC++でいうlong long intの最大値より大きな値を扱いたい場合に使用する。long long intの最大値は、(2^64) / 2 - 1でだいたい8.0 * 10^18。
話はちょっとそれるが、『2^10 ≒ 10^3』と覚えておくと累乗の計算はすぐできるので便利。

2つ目は、BigDecimal。これはdoubleより精度の高い小数を扱うときに用いる。
BigIntegerの概念は分かりやすいが、BigDecimalは・・・。はて??doubleの精度ってそもそもどのくらいだったっけ?
ということで、今日は、
  1. doubleの最大値、最小値
  2. doubleの精度
  3. 丸め誤差
について、実際にPC上でプログラムを走らせながら検証します。(ちょっと長くなりそうだな~)

その前に、doubleの内部構造についておさらいします。doubleは1ビットの符号部、11ビットの指数部、52ビットの仮数部から成り立っています。
(詳しくは、参考ページをご参照ください。)
簡単に言うと10進数の指数表記(2.999 * 10e+10みたいなやつ)と同様のことを2進数でやってるだけです。

では、準備もできたので、まずは、doubleの最大値、最小値を求めてみましょう。指数部が11ビットなので概算では大体2^1024くらいがdoubleで表すことのできる最大値です。だいたい10^300くらいですねー。最小値は、2^(-1024)なので、10^(-300)程度だと思います。
では実際に計算しましょう。


  1. #include <float.h>  
  2.   
  3. #define EXP_MAX 2046     // 2047 is booked for exception procedure  
  4. #define EXP_MIN 1        // 0 is booked for exception procedure  
  5. #define EXP_BIAS 1023    // The biased exponent range: [-1022, 1023]  
  6. #define SIG_BIT 52       // significand bits  
  7.   
  8. int main() {  
  9.    // max  
  10.    double dblmax = pow(2.0, EXP_MAX - EXP_BIAS);  
  11.    double significand = 1.0;  
  12.    REP(i, SIG_BIT)  
  13.        significand += pow(2.0, -(i+1));  
  14.    dblmax *= significand;  
  15.    printf("dblmax = %e, DBL_MAX = %e\n", dblmax, DBL_MAX);  
  16.   
  17.    // min  
  18.    double dblmin = pow(2.0, EXP_MIN - EXP_BIAS);  
  19.    printf("dblmin = %e, DBL_MIN = %e\n", dblmin, DBL_MIN);  
  20.   
  21.    return 0;  
  22. }  


んー、実際は、最大値が1.797693e+308最小値が2.225074e-308でした。ほぼほぼ予想通りです。指数部の値は、0と1024が例外処理のため予約語となっているようです。(参考ページ参照のこと。)やられた。これに気付かず1時間ぐらい悩んでしまった。。。

次に、doubleの精度を考えてみましょう。指数部が数値のスケール、仮数部が数値の精度を表しているので仮数部に注目すればOKです。52ビットなので、およそ10^15程度(有効数字15桁)の精度は持っているのではないでしょうか。(経験的にはそんなに無い気が・・・)実際にPC上で確認してみましょう。


  1. int main() {  
  2.    double min_significand = pow(2.0, -SIG_BIT);  
  3.    printf("%.16f\n", min_significand);  
  4.   
  5.    double x = 1e15 + 1.0;  
  6.    double y = 1e16 + 1.0;  
  7.    printf("%.1lf\n", x);  
  8.    printf("%.1lf\n", y);  
  9. }  


んーと、仮数部の最小値は2.0 * 1e-16程度でした。スケールに対して2.0 * e-16程度の大きさの値までしか表現できないという意味なので、有効数字は15桁と考えていいでしょう。
実際に上のソースで検証しましたが、1.0 e+16に1を加算しても無視されてしまいます。
あれ、そうか!これが世間で言われている『情報落ち』ってやつか!!

最後に「丸め誤差」について。言わずと知れた「10進数を2進数に変換するときにでる誤差」のことです。代表的な例に、0.1を10回足しても1にならないというのがあります。
今回は、0.1をdoubleで表したときにどのような表現になるのか、またその値は10進数ではどの程度なのか調べたいと思います。標準関数frexp()を使えば簡単にできるようですが、なかなか面白そうなテーマなので、今回は自力で実装します。
ポイントは、『仮数部は52ビットあるので全件探索ではNG。何らかの”工夫”が必要』ってことです。まーでもこの場合は工夫と言ってもアレしかないですね(笑)


  1. double getSigVal(LL sigbit) {  
  2.    double ret = 1.0;  
  3.   
  4.    REP(i, SIG_BIT) {  
  5.        if ((sigbit >> SIG_BIT - 1 - i) & 1)  
  6.            ret += pow(2.0, -(i+1));  
  7.    }  
  8.    return ret;  
  9. }  
  10.   
  11. double getDoubleVal(int exp, LL sigbit) {  
  12.    return getSigVal(sigbit) * pow(2.0, exp - EXP_BIAS);  
  13. }  
  14.   
  15. int main() {  
  16.    double x = 0.1;  
  17.    LL sig, sigl, sigr;  
  18.    int expo = 1;  
  19.   
  20.    while (pow(2.0, expo + 1 - EXP_BIAS) <= x)  
  21.        expo++;  
  22.   
  23.    sigl = 0LL;  
  24.    sigr = (1LL << 52) - 1LL;  
  25.    sig = (sigl+ sigr) / 2;  
  26.   
  27.    REP(i, 200) {  
  28.        double val = getDoubleVal(expo, sig);  
  29.        if (ABS(val - x) < 1e-18)  
  30.            break;  
  31.        else if (val > x)  
  32.            sigr = sig;  
  33.        else  
  34.            sigl = sig;  
  35.        sig = (sigl + sigr)/2;  
  36.    }  
  37.   
  38.   
  39.    printf("significand = %lld, exponent = %d\n", sig, expo);  
  40.    printf("0.1 is expressed something like %.18lf on the PC\n", getDoubleVal(expo, sig));  
  41.    return 0;  
  42. }  

52ビット(10進数で10^15程度)を線形探索していたのでは、時間がありません。Binary Searchで解きましょう。精度は1e-18程度でいいでしょう。ループは200回くらい回せば探索範囲が1e-60倍くらいに縮小するので200回くらい回せばこの場合は十分です。(Binary Searchはバグを生みやすいので、無限ループはやめて必ず有限回の探索で打ち切るようにしましょう。
参考ページ:
doubleの内部構造
指数部の例外処理予約値

0 件のコメント:

コメントを投稿