NaN boxingとsizeof(mrb_value)

mrubyはオブジェクトの型としてmrb_valueというものを使う。これは以下のような宣言になっている。

typedef struct mrb_value {
  union {
    mrb_float f;
    void *p;
    mrb_int i;
    mrb_sym sym;
  } value;
  enum mrb_vtype tt:8;
} mrb_value;

共用体valueに4つの型があり、intだったりポインタだったりする。前にも書いたような気はするが、この中でmrb_floatがdouble型で定義されており(MRB_USE_FLOAT未定義の場合)、その8byteとttの1byte、さらに8byteアラインされてこの構造体のサイズは合計16byteになる。
ttとvalueを別々に操作する場合(OP_MOVEなど)ならいいが、関数の引数や戻り値にmrb_valueを値渡しで使うとスタック経由で16byteが引き渡されるし、mrb_valueをコピーすると16byteのコピーとなる。これが非常に効率悪い。
この対策としては、例えば関数の引数をttとvalueに分けるという手が考えられるが、戻り値を2つにすることはできない。戻り値を入れるアドレスを渡すようにするのもいい。でも今から全部直すのは大変だ。
そこで。
NaN boxingで対策することを考えてみる。NaN boxingについて日本語の情報はほとんどないが、ここに大変わかりやすく書いてあるので興味がある人は見てみるのをオススメ。
http://d.hatena.ne.jp/Constellation/20110910/1315586703
これを利用すればmrb_valueは8byteになる。簡単に説明すると、double型の値は使われていない値が多いのでそれを活用して別の値を埋め込もうという作戦だ。

ただ、いじってるけどまだきちんと動いていないので、どのように実装するのかという考えかたをメモ。作ってみたい人の参考(反面教師かも)になるとよい感じだけどどうだろう。


基本的な考え方は先ほどのブログを参考にしてもらって。方針としては32bit限定として。
まず、mrb_valueを改造する。しばらくはmruby.hのコード。

typedef struct mrb_value {
  union {
    union {
      mrb_float f;
      void *p;
      mrb_int i;
      mrb_sym sym;
    } value;
    struct {
      int v;
      enum mrb_vtype tt;
    };
  };
} mrb_value;

元のコードは上のほうにあるので比べてみるといい。無名共用体と無名構造体を使って、64bitのdouble領域の上32bitにttを割り当てた。double以外は下32bitが使われるので、ttが大きくなっていても影響は無い。double型の場合はttと重なるが、そこはNaN boxingでうまいことやる。エンディアンの問題とかありそうだ。
次にマクロ系を。

//#define mrb_type(o)   (o).tt
//#define mrb_nil_p(o)  ((o).tt == MRB_TT_FALSE && !(o).value.i)
//#define mrb_test(o)   ((o).tt != MRB_TT_FALSE)
#define mrb_type(o)   ((unsigned int)0xfff00000 < (unsigned int)(o).tt ? (o).tt : MRB_TT_FLOAT)
#define mrb_nil_p(o)  (mrb_type(o) == MRB_TT_FALSE && !(o).value.i)
#define mrb_test(o)   (mrb_type(o) != MRB_TT_FALSE)

符号なし32bitでttが0xfff00000より大きければそれはNaN boxingで割り当てられたdouble以外の値である。それ以下の場合はFloatオブジェクトだと言える。また、ttを直接見ている部分は困ることになるのでmrb_typeマクロを使うように変えた。これは他にもあちこちあるのであちこち変えた。
このコードを見ると、0xfff00000より大きな値がttとして返るように見える。そう思ったのならそれは正解。

enum mrb_vtype {
//  MRB_TT_FALSE = 0,   /*   0 */
  MRB_TT_FALSE = 0xfff00001,   /*   0 */

mrb_vtypeの値をNaN boxingで使える値の範囲にした。これがttの32bit化の理由だ。
あと変えたのはここ。

static inline mrb_value
mrb_float_value(mrb_float f)
{
  mrb_value v;

//  v.tt = MRB_TT_FLOAT;
  v.value.f = f;
  return v;
}

1行消した。NaN boxing化でdouble型はそのまま入れればFloatオブジェクトになるのでttをセットする必要が無い。
これ以外の修正はvm.cのSET_FLOAT_VALUEからttの設定を消したのと、あちこちのttの参照をmrb_typeマクロを使うように変えたのと、object.[ch]のttの型を32bitにしただけだ。修正が少なくなるように細工してみたら結構少なかった。
とはいえ、何か見落としているようで動かすと文字列リテラルのpool周りでコケる。

D:\test>mruby test.rb
2.03125
D:\test>mruby_test test.rb
1.703125

速いのは速いんだけどね。理論的にはMRB_USE_FLOATを定義したのと同じ転送サイズになるから、それよりやや遅いぐらいにはなるんじゃないかと。
ああ、あとこれNaNの正規化とかしてないから、そのへんも作らないとNaN boxingとしては片手落ちかも。