Flonum のビットレイアウト
double から VALUE の話を考える。
Flonum は下位 2 bit を 0b0010 にする、という話だった。では、その 2 bit はどこから来るか、というと、指数部 11 bit の上位 2 bit を使う、ということだった。指数部の上位 2 bit は 61, 62 bit 目にあるので、さて VALUE にはどうやってエンコードするか?
s xx eeeeeeeee m...m (m: 52bit) s: 符合 x: 使わない e: 指数部 (9bit) m: 仮数部 (52bit)
いくつか選択肢がある。一番素直なのは、0, 1 bit 目の仮数部を、61, 62 bit 目にコピーしてしまうことだ。
s mm eeeeeeeee m...m 10
このようにエンコードすれば、62 bit で表現できる。が、ビットの移動はちょっと計算が面倒だ。もうちょっとなんとかならいだろうか。
ビットのローテートを考えてみよう。ビットをぐるーっと回す奴。左に 3 bit 回してみると、こんな感じになる。
e...e m...m s 01
戻すときは、右に 3 bit 回せば良い。簡単そうだ。CPU によっては(というか、x86 には) rot 命令があるので、1命令で実現できる。
bias = 1024 p 0b00000000000 - bias # -1024 p 0b01000000000 - bias # -512 p 0b01100000000 - bias # -256 Flonum p 0b10000000000 - bias # 0 Flonum p 0b10011111111 - bias # 255 Flonum p 0b10100000000 - bias # 256 p 0b11000000000 - bias # 512 p 0b11111111111 - bias # 1023
Flonum になるのは、指数部が -256~255 の間だから、どうやら、Flonum になるのは、先頭 bit が 011 or 100 の時だけらしい。ここで、e のビット列の最上位ビットを e10, その下位ビットを e9、... と名付けることにする。このとき、e10, e9 は 01 or 10 であり、e8 が 1 の時は 01、0 の時は 11 であることがわかる。
さて、ここまでわかったら、実際にコードを見てみよう。実装しているのが次のコード(ruby/internal.h at trunk · ruby/ruby · GitHub):
static inline VALUE rb_float_new_inline(double d) { #if USE_FLONUM union { double d; VALUE v; } t; int bits; t.d = d; bits = (int)((VALUE)(t.v >> 60) & 0x7); /* bits contains 3 bits of b62..b60. */ /* bits - 3 = */ /* b011 -> b000 */ /* b100 -> b001 */ if (t.v != 0x3000000000000000 /* 1.72723e-77 */ && !((bits-3) & ~0x01)) { return (RUBY_BIT_ROTL(t.v, 3) & ~(VALUE)0x01) | 0x02; } else if (t.v == (VALUE)0) { /* +0.0 */ return 0x8000000000000002; } /* out of range */ #endif return rb_float_new_in_heap(d); }
bits = (int)((VALUE)(t.v >> 60) & 0x7)
で bits に e10, e9, e8 を入力している。で、3 を引いてやると、0b011 (3) か 0b0100 (4) は 0 or 1 になる。つまり、下位 1 bit を 0 にすれば、0 になるはずである。それを計算しているのが (bits-3) & ~0x01)
。0 以外であれば、Flonum 対象外ということだ。rb_float_new_in_heap()
に処理を逃がしている(実装方法はまた今度)。
t.v != 0x3000000000000000 /* 1.72723e-77 */
の部分はよくわからないんだけど、どうやら、e の先頭が 0b011 であっても、その中でも最小の値は、これだとうまくいかないらしい。なんでだろう? うーん、なくてもうまくいきそうなんだけどな。ログを見ても、最初からこのチェックがあったようだ。うーん、なんでだろ? 自分で書いたのにね。
逆に、Flonum value -> double に変換するのは次のコード https://github.com/ruby/ruby/blob/trunk/internal.h#L1737:
static inline double rb_float_flonum_value(VALUE v) { #if USE_FLONUM if (v != (VALUE)0x8000000000000002) { /* LIKELY */ union { double d; VALUE v; } t; VALUE b63 = (v >> 63); /* e: xx1... -> 011... */ /* xx0... -> 100... */ /* ^b63 */ t.v = RUBY_BIT_ROTR((2 - b63) | (v & ~(VALUE)0x03), 3); return t.d; } #endif return 0.0; }
e8 を見ると、e9, e10 に何を補完すれば良いかわかる、ってのが、コメントに書いてあること。つまり、b63 ってのは e8 のことだけど、それが 0 なら e10 e9 は 0b10 で、1 なら 0b01 である。これは、2 (0b10 - e8)で計算できるわけで、それが (2 - b63) | (v & ~(VALUE)0x03)
。それをぐるっと右に 3bit 回せば元の double ができあがり。
ちょっと面倒だったね。
あー、今気づいたんだけど、t.v != 0x3000000000000000 /* 1.72723e-77 */
のチェックって、Flonum VALUE 0x8000000000000002 を double 0.0 に対応づけるための仕組みかー。うーん。なるほど。0.0 はよく使うので、Flonum でなんとか表現したかったんですよね。