きどたかのブログ

いつか誰かがこのブログからトラブルを解決しますように。

浮動小数点やBigDecimalに関して

意見が噛み合わない。
僕はきっと正しいはずだ。
1d/3dが完璧な値を保持してると寝言を言う人に負けるな自分。


小数点演算において、1÷3は無理数である。
無理数を有限数のbitで表現する術はない。
故に1d/3dは丸まっている。
ちなみに、1dは有理数、3dも有理数です。


倍精度浮動小数点は符号部、指数部、仮数部で構成される。
基数は2です。バイアスは1023です。
doubleは64bitで構成されています。
1bitの符号、11bitの指数、52bitの仮数


ではなぜ、1d/3dの結果に対して、3dを乗じると1.0に戻るのか。


public static void dumpDouble(double d) {
// 16 進数文字列表現(pの後が指数)
String hexString = Double.toHexString(d);
System.out.println("hexString:" + hexString);
// ダブルフォーマット ビットレイアウト
long longBits = Double.doubleToLongBits(d);
System.out.println("longBits:" + longBits);
// 符号X
double sign = Math.signum(d);
System.out.println("sign:" + sign);
// 指数
long exponent = Math.getExponent(d);
System.out.println("exponent:" + exponent);
// 仮数
double mantissa = d / Math.pow(2, exponent);
System.out.println("mantissa:" + mantissa);
}

    • 1d--

hexString:0x1.0p0
longBits:4607182418800017408
sign:1.0
exponent:0
mantissa:1.0

    • 3d--

hexString:0x1.8p1
longBits:4613937818241073152
sign:1.0
exponent:1
mantissa:1.5

    • 1d / 3d--

hexString:0x1.5555555555555p-2
longBits:4599676419421066581
sign:1.0
exponent:-2
mantissa:1.3333333333333333


仮数部の小数点以下は16桁フルフル使ってる。
フルフルってのは、52bitだと16桁が限界だってことだ。
(intは32bitで約21億、10桁ってとこ。)
このように仮数の情報が足らなくなっています。これは「丸め誤差」でしょう。
いったん丸まった値のくせして、3dを乗じると1.0に戻る。
これは戻す時にまた誤差が出たと思うのが妥当だろう。
仮数を考える。
1.3333333333333333 * 1.5 = 1.99999999999999995
これは、仮数としてのbit数で表現出来ない値だ。
これは「直近への丸め」が動いたと思う。(The Java Language Specification,Third Edition #4.2.4)
恐らくは1.99999999999999995を2で割って、
0.999999999999999975なんだが、1の方へ丸まったんでしょうよ。


BigDecimal bd = new BigDecimal("0.999999999999999975");
double d = bd.doubleValue();
System.out.println(d);
この結果は1.0を出す。
んー、やっぱりか。


たまたま元の1に戻ったからと言って、丸まっていないということではない。
「丸め誤差」が発生し、戻す際にそれをたまたま打ち消す「直近への丸め」が動いたんだ、きっと。
独学だから合ってるかは知らないが、まあ理屈は合ってるはず。


あと、new BigDecimal(1d / 3d)はこんな値。
0.333333333333333314829616256247390992939472198486328125
精度:54
スケール:54


BigDecimal three = BigDecimal.valueOf(3);
BigDecimal divide = BigDecimal.ONE.divide(three, 54, RoundingMode.HALF_EVEN);
System.out.println(divide);
0.333333333333333333333333333333333333333333333333333333
doubleとBigDecimalでは扱えるbit数が全然違うので、
情報としてはより期待している値が返ってくる。



BigDecimalがDecimalと付いている関係で、10進数で持っていると思われている節がある。
BigDecimalは、クラス説明にもある通り、任意精度の「スケールなし整数」とintのスケールで構成されている。
任意精度の「スケールなし整数」はBigIntegerである。
BigIntegerは内部的にint[]を持ち、それは2の補数表現で値を保持していたはず。
intをbit表現として使用している。


BigDecimalにDecimalと付いていることの正しい認識とは何か?
それはスケール操作は10を基数にしているということだ。
別に内部がパック10進数だとかゾーン10進数ってことじゃない。
ということを知らない人と議論するのが面倒臭い。
なんか空しくなる。
僕よりもこの分野で詳しい人がいないことには、正しいことを言っても誰も理解を示してもらえないだろう。


BigDecimalの最大値の理論上の値を出して、
「OutOfMemoryErrorになるから試験不能です。やるならメモリ買ってください。」なんて言ったもんだからアラ大変。
一生の汚点となるようなコードを書かないといけなくなりそうだ。
オレは責任取らない。取れない。やだ。書けるけど書きたくない。