Symbol

Symbol というと、Ruby では :sym のように書く奴。 こいつには

  • 静的シンボル
  • 動的シンボル

の2種類ある。STATIC_SYM_P(v) でチェックすることができる。ここでは、静的シンボルだけ説明する。

静的シンボルは、絶対に GC されないシンボル。メソッド名などで使ったシンボルが、これにあたる。理論的にはこいつらも GC 可能にできるんだけど、そのためにはとても大変な改修が必要なので、GC しないで楽をしよう、ということになった。そのため、define_method なんかを使って無限にメソッド名を作っていくと、undef しててもメモリがパンクする。

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

Flonum

Fixnum を紹介したので、次は Flonum を紹介する。

Flonum は聞き慣れない言葉ではないかと思うのだが、Float オブジェクト(浮動小数点数オブジェクト)を Immediate として表現するためのものだ。 Fixnum は Integer の一部(63bit で表現できる整数値)だったが、Flonum も同様に、浮動小数点数の一部を軽量に表現するものになる。

64bit CPU 限定で動作する(32bit CPU では有効にならない)。

Ruby浮動小数点数IEEE 754 - Wikipedia 倍精度浮動小数点数で表現されるので、64 bit 幅が必要になる。VALUE 幅は 64bit CPU だと 64 bit なので、だいぶ近い。少し頑張れば入りそうではないか? と思って頑張って入れたのが Flonum。

さて、どうやって入れるか。まず、Immediate でなければならず、さらに Flonum であることを示さなければならないため、下位 2 bit は 0b10 となっている。つまり、(v & 0x03) == 0x02 であれば、Flonum ということになる。

次は、64bit 倍精度小数点数(面倒だから、double と書こう)をどうやって 62 bit に押し込むか。

IEEE 754 double の表現は、次のようになっている。

  • s: 符号 1 bit
  • e: 指数 11 bit
  • m: 仮数 52 bit

の合計 64 bit で構成されている(s から上位ビット)。数値は、-1s * 2e-1024 * m (m = 1 + Σ i=0; 51; bi/(252-i)) で得られる。数式は適当に書いているし、なんか難しそうだけれど、e がどれくらい大きいか、m がどれくらい精度を持つか、で覚えておけばいい。

さて、どこから2ビットを削るか、というのが問題。仮数部が一番ビット数多いんだけど、仮数部を2ビット減らすと、つまり精度が落ちることになる。「Ruby で計算すると、なんか変な値になる」(変、とは...)、ってなるのは嫌だ。

それなら、e を削る方がいいかな、ということで、e から 2 bit 拝借することにした。double は指数部として 2^-1024 ~ 21023 の範囲を表すことができるが、2^-256~2255 の範囲だけ Flonum で表現できればいいか、と考える。実際、その範囲はどれくらいよ、ということなんだけど、2256 = 1.1579208923731619542357098500869e+77 らしく、まぁ、普通の人はこんなに大きな数も使わないよな、ということで、多分大丈夫なんだと思う。

e は 11 bit なので 0~2047 の範囲が表現できるが、バイアス 1024 を引くので、指数として表現出来るのは -1024 ~ 1023 になる。今回は、指数が -256 ~ 255 なので、

if (e >= -256  && e<= 255) {
  // flonum
}
else {
  // flonum じゃない格納方法
}

みたいに分けることになる。さて、2ビットを削った Flonum のビットレイアウトはどのようにすれば良いか。続きは次回。

風邪とか

子供が風邪をひいて移されて、それから妻にうつったりして大変だったなぁ、と思ってたらこのブログのことをすっかり忘れていて、なんと20日弱もサボっていた。 いやー、やっぱり続けるの難しいなあ。頑張ります。

Fixnum

IMMEDIATE_P(v) でもっともポピュラーなものは Fixnum だろう。下位 1 bit が 1 であれば(v&1 であれば)Fixnum だ。

さて、そもそも Fixnum ってなんだっけ。Ruby 2.4 で消滅したクラスだった。範囲つき整数であり、範囲を超えると Bignum になった。Bignum は、メモリがある限り、(絶対値が)大きな整数を表現できる。Ruby 2.4 では、Fixnum と Bignum は消滅し、Integer に統合された。実装が違うだけで、同じ整数を表現するため、そこに別々のクラスを用意するのは、あまり意味が無いだろう、ということで統合された(もともと、Bignum と Fixnum の親クラスとして Integer クラス自体は存在していた)。Ruby プログラマからみると、Fixnum と Bignum はシームレスに変換されるため、意識することはない、と思う。厳密には、同じ値なのに別々の object_id が帰ってくることがあったりするのが Bignum が Fixnum と同じところだ。

Bignum と Fixnum は Integer に統合されたが、実装レベルでは依然としてこの二つは残っている。Bignum はメモリを確保するので毎回メモリアロケーションするが、Fixnum は special const なので、VALUE に埋め込まれるのでメモリ確保は不要である。

さて、Fixnum の範囲は何かというと、63bit か 31 bit で表現できる値となる。LSB 1 bit を識別のために利用するので、63 or 31 bit しか利用できないのだ。

#define RB_INT2FIX(i) (((VALUE)(i))<<1 | RUBY_FIXNUM_FLAG)
#define INT2FIX(i) RB_INT2FIX(i)

C の int の値を、Fixnum でその値を表現する VALUE に変換するには、この INT2FIX(v) を利用する。1 bit 左シフとして(2倍して)、RUBY_FIXNUM_FLAG つまり 0x01 を or する。簡単。同じく、Fixnum から int に変換するには 1 bit 右シフトすれば良い。

VALUE に埋め込む値なので、Fixnum として存在できるオブジェクトの数は決まっている。逆の言い方をすると、Fixnum オブジェクトは、起動時にすでにすべて作成されているとも言える。1 という数値リテラルは、MRI から検索し、存在した Fixnum オブジェクトを取り出した、とも言える。

true

IMMEDIATE_P(v) は、いくつかの種類のオブジェクトであると紹介した。もっとも簡単なものは true であり、Qtrue というマクロで値を参照できる。実際には 0x14 == 0b0001_0100 という値になる(64bit CPU の場合)。

#define RB_IMMEDIATE_P(x) ((VALUE)(x) & RUBY_IMMEDIATE_MASK)
#define IMMEDIATE_P(x) RB_IMMEDIATE_P(x)

という定義だったのを思い出すと、immediate mask は 0b0111 なので、0x14 & 0b0111 はちゃんと !0 であることがわかる。つまり、IMMEDIATE_P(Qtrue) は真である。

ここまでで、true, false, nil が出揃った。

  • true: Qtrue == 0x14
  • false: Qfalse == 0x00
  • nil: Qnil == 0x08

RB_TEST_P(x)

https://github.com/ruby/ruby/blob/ruby_2_6/include/ruby/ruby.h#L478

#define RB_TEST(v) !(((VALUE)(v) & (VALUE)~RUBY_Qnil) == 0)
#define RB_NIL_P(v) !((VALUE)(v) != RUBY_Qnil)
#define RTEST(v) RB_TEST(v)
#define NIL_P(v) RB_NIL_P(v)

RB_TEST(v) の定義はこれだった。これは、x が false か nil 以外だったら真、というもの。Ruby では nil もしくは false だったら偽、という定義と同じものだ。C コード中で if (RB_TEST(v)) {...} みたいに、Ruby 的に真だったら、みたいに書く時に使う。

ついでに、NIL_P(v) の定義もある。Qnilそれと等しいかどうか、をチェックしている。いちいち、!(v != Qnil) と二重否定にしているのは、... なんで?

https://github.com/ruby/ruby/commit/8497a7b9187a05210a4f6b842d06d1268d704d35

if NIL_P(x) { ... } と(if RTEST(x) { ... } だったっけ? という話も)C で書いたパッチが来たから、括弧を省略できないようにしたらしい...。

そういえば、例によって RB_TEST に RTEST、RB_NIL_P に NIL_P、RUBY_Qnil には Qnil という別名がついている。歴史的には逆で、先に RTEST とかがあった。RubyAPI であることを強調するために RB_ prefix を付けている。

さて、RB_TEST(v) の実装に戻ると、!((v & ~Qnil) == 0) だったら真らしい。このビット演算は何をしているのか?

Ruby において、偽の値は falesnil で、それぞれ C では QfalseQnil という値である。つまり、素直に書くと、!((v) == Qnil) || (v) == Qfalse) とすれば良い。それがなぜ論理演算になるのか?

実は、Qfalse は 0 で、Qnil0b0100 == 0x08 という値である。展開すると !(v == 0x08 || v == 0x00) こうなる。つまり、v が 0b0000 もしくは 0b0100 であれば偽ということになる。この二つの違いは 0b0100 の 1 bit だけなので、この bit 以外が 0 であれば、Qfalse もしくは Qnil である、と言える。つまり、v の 3 bit 目を 0 にしておけば、あとは全体が 0 であるかをチェックすれば偽であると言える。これを実装するのが (v & ~Qnil) == 0 という式である。

なぜ論理式にするか、というと、分岐が1個減って、もしかしたらちょっと速くなるかも、ということで、こういう式になっている。

ちなみに、gcc では、最適化オプションによって、素直に (v == Qnil || v == Qfalse) と書いても (v & ~Qnil) == 0 と同じコードが出力される。賢い。clang では、真面目に2回チェックしていた。