「なぜJavaScriptで「76287755398823936」が正しく表示できないか、あるいはなぜRubyでも表せないか。」の続きです。後半戦、テンションあげてまいりましょー(涙目
■出力側ソースコードのチェック!
さて…では重い腰を上げてソースコードを読みましょうか…。 FirefoxでもChromeでも起きるなら、何かWindowsのライブラリのバグ…なんでしょうか。ま、いいや。とりあえずソースコードが探しやすそうなChromeから見てみましょう。
それっぽいメソッドを探していくと…見つかりました。これですね。v8::internal::Grisu3()です。…あれ…?標準ライブラリじゃ…ない…!?うげぇめんどくさい…
v8をWindows上でコンパイルするのはひたすら☆面倒†1なので、Ubuntu上でコンパイルしてCodeLiteというIDEをGDBのGUIラッパーとして使いました。これ、初めてだったんですが結構便利。Windowsでも使えるならちょっと試してみようかなってレベルです。EclipseCDTとは何だったのか。あとDDDとxxgdbはクソ。
■原因は、やっぱり精度の問題(
これも、実はやっぱりというか結局というか、doubleの精度の問題です(
この問題の値をintegerからdoubleに一意に表現することができるのですが、doubleからintegerには必ずしも一意に変換はできない、ということです。
■何もかもを忘れてさっきのdoubleの表現から元の値を読みだそうとしてみる
これをやるとどうしてなのかわかります。
0/10000110111/0000111100000111010011110011000101000010010000000000
今までの事は一切わすれて。この値、いったい何なんでしょうね?早速IEEE754に基づいて分析してみよう!(投げやりな態度で)
ふむふむ…符号は0だから、プラスの数みたいですね。
指数は1079だから…1023でバイアスされてるから本当は1079-1023=56なんですね。メモメモ。
仮数は0b0000111100000111010011110011000101000010010000000000だから、二進の小数で
1.0000111100000111010011110011000101000010010000000000(53桁)
なんですねですね。へー。
それで、このdoubleが示す値は
(-1)符号 * 仮数 * 2指数
で表現されるから…
+1.0000111100000111010011110011000101000010010000000000 * 256
=10000111100000111010011110011000101000010010000000000xxx
…ん?xxx??
■「1」と「1.00」は全然違う!
高校や大学の時に、物理の時間とかで、定規で測った値を「1cm」とかって書いたら怒られませんでした?「この定規、ミリの目盛りまであるじゃん。1cmぴったりだったんなら、1.0cmって書かないとだめだよ!」とかって言われた記憶、ありません…?†2
今回もそれと根っこは同じことです。「x=1」と書いたら数学的には「x=1.000000000000000000….」だけど、物理学的にはそうではなく「0.5 <= x < 1.5」のあいだであるのと同じ。
今回の例で言えば、
1.0000111100000111010011110011000101000010010000000000
の”本当の値”は
1.00001111000001110100111100110001010000100011111111111以上、
1.00001111000001110100111100110001010000100100000000001未満
の間にある!って事です(逆に言えば、これらの値をdoubleにすると皆同じdoubleの表現に落ちる、ということです)。これをそれぞれさっきのに代入すると…
+1.00001111000001110100111100110001010000100011111111111 * 256
=76287755398823928
+1.00001111000001110100111100110001010000100100000000001 * 256
=76287755398823944
ほう…そう来ましたか…
とうわけで、このdoubleが示すxは、
「6287755398823928 <= x < 76287755398823944」
なんですね。そう、やっぱり「76287755398823936」には決まりませんでした。
この時、62877553988239**という桁までは確定、その次の桁は2か3か4、最後の下一桁はさっぱり☆わからない、というわけですが、stringに変換する際はどれかには決めないといけません。この時に最後を40(上側)とするのがChrome/(とたぶんFirefoxとRuby)、30(真ん中)に多分するのがIE8/9、ということになります。でもIEの挙動はなんか標準じゃ無いっぽい…?
IEEE754での標準の丸めモードは「最近接丸め(偶数):最も近くの表現できる値へ丸める。表現可能な2つの値の中間の値であったら、一番低い仮数ビットが0になるほうを採用する。」との事なので、切り捨てられがちになるから上の方の値を採用してるって事なんでしょうかね、たぶん。
ちなみに、この解説した部分がissueで示した、v8::internal::Grisu3のboundary_minusとboundary_plusを計算する部分です。V8では、boundary_plusの方から文字列を作っているので、ソースを読みたい場合はboundary_plusに着目していってみてください。
あとこの実装の理論的詳細はこちら「Florian Loitschの論文”Printing floating-point numbers quickly and accurately with integers”」にあるそうです。
■もっと単純に表現すると
「76287755398823936=0b100001111000001110100111100110001010000100100000000000000」は最後の0まで意味があるので、43ビット精度の数ではなくてやっぱり57ビット精度の数です。
つまり、64ビットの浮動小数点の精度53ビットでは足りないので、うまく表現することはできません。
■でも「76287755398823936」って出てほしい!!
1cmって書いたら1.0cmだし1.00cmだって言いたくなるのも人情(たぶん)。そう表現したい場合は、
ってやってみてください。詳しい仕様はこちら!そして今までの、ToStringをnumberに適用した場合の仕様はこちら。
■まとめ
- JavaScriptで整数を扱うときは53ビット整数までにしよう(あれ…?普通だ…?)
- 浮動小数点は結構厄介。でもたまに見る分には面白いね!
■早速v8のissueに回答もらった
Precise representation of this number requires 57 bits not 43.
When formatting a string representation of double we are free to choose any string that when parsed back would give the same double number. Simply speaking only the following must hold:
parseFloat(d.toString()) == d
In this case both 76287755398823940 and 76287755398823936 map to the same double number and we can use either of them when printing the result back.
You can use d.toFixed(0) to uses different formatting algorithm for numbers less than 10^21. This algorithm will render double with mantisa m and positive exponent p exactly as integer m*2^p. In you case it means that
(76287755398823936).toFixed(0) === “76287755398823936”
For more information please read:
ECMA-262 5th 9.8.1 ToString Applied to the Number Type. http://es5.github.com/#x9.8.1
ECMA-262 5th 15.7.4.5 Number.prototype.toFixed (fractionDigits) http://es5.github.com/#x15.7.4.5
Details about the algorithm used by V8 are available in Florian Loitsch’s paper “Printing Floating-Point Numbers Quickly and Accurately with Integers”.
こんなに詳しく教えてもらえるなんてほんとありがたいです…。しかしそれに対する私の返答はひどすぎ…。こういうのあるともっと英語を知らないとな!って思います…。