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 でなんとか表現したかったんですよね。