Ruby1.9ではObjectのC表現RObject構造体がかなり変わっています。参考までにRuby1.8のRObject構造体はこうです。
struct RObject { struct RBasic basic; struct st_table *iv_tbl; };
シンプルです。次にRuby1.9のRObject構造体です。*1
#define ROBJECT_EMBED_LEN_MAX 3 struct RObject { struct RBasic basic; union { struct { long len; VALUE *ptr; } heap; VALUE ary[ROBJECT_EMBED_LEN_MAX]; } as; }; #define ROBJECT_EMBED FL_USER1 #define ROBJECT_LEN(o) \ ((RBASIC(o)->flags & ROBJECT_EMBED) ? \ ROBJECT_EMBED_LEN_MAX : \ ROBJECT(o)->as.heap.len) #define ROBJECT_PTR(o) \ ((RBASIC(o)->flags & ROBJECT_EMBED) ? \ ROBJECT(o)->as.ary : \ ROBJECT(o)->as.heap.ptr)
すっげー複雑です:-)。読解しましょう。
Ruby1.8のiv_tblはインスタンス変数のテーブルです。というわけでインスタンス変数を操作する関数を見てみましょう。
switch (TYPE(obj)) { case T_OBJECT: klass = rb_obj_class(obj); if (!RCLASS_IV_INDEX_TBL(klass)) RCLASS_IV_INDEX_TBL(klass) = st_init_numtable();
Ruby1.9ではRObject単独でインスタンス変数を管理するのではなくRClassと連動するようです。RClassの定義は以下の通り。*2
typedef struct { VALUE super; struct st_table *iv_tbl; } rb_classext_t; struct RClass { struct RBasic basic; rb_classext_t *ptr; struct st_table *m_tbl; struct st_table *iv_index_tbl; }; #define RCLASS_IV_INDEX_TBL(c) (RCLASS(c)->iv_index_tbl)
先に進みましょう。
ivar_extended = 0; if (!st_lookup(RCLASS_IV_INDEX_TBL(klass), id, &index)) { index = RCLASS_IV_INDEX_TBL(klass)->num_entries; st_add_direct(RCLASS_IV_INDEX_TBL(klass), id, index); ivar_extended = 1; }
変数名を示すidからindexを拾う、ないのならエントリの追加を行っています。つまり、RClass.iv_index_tblはこういう構造なようです。
key(id) | value(index) |
ID(x) | 0 |
ID(y) | 1 |
len = ROBJECT_LEN(obj); if (len <= index) { VALUE *ptr = ROBJECT_PTR(obj); if (index < ROBJECT_EMBED_LEN_MAX) { RBASIC(obj)->flags |= ROBJECT_EMBED; ptr = ROBJECT(obj)->as.ary; for (i = 0; i < ROBJECT_EMBED_LEN_MAX; i++) { ptr[i] = Qundef; } }
初めてrb_ivar_setが呼ばれた場合、ROBJECT_EMBEDフラグが立っていない*3のでROBJECT_LENマクロはas.heap.lenを選択しますがこちらも0です。というわけでifの中に入ります。ROBJECT_PTRも同様にas.heap.ptrを返すのですが1個目のインスタンス変数はまだ埋め込みVALUE配列に収まるので調整が行われています。
埋め込みVALUE配列に収まらない場合、ヒープにVALUE配列が確保されています。
else { VALUE *newptr; long newsize = (index+1) + (index+1)/4; /* (index+1)*1.25 */ if (!ivar_extended && RCLASS_IV_INDEX_TBL(klass)->num_entries < newsize) { newsize = RCLASS_IV_INDEX_TBL(klass)->num_entries; }
番号テーブルは全インスタンスで共有されているので、新しい変数が追加されるわけではない場合は現在の番号テーブルサイズがヒープVALUE配列のサイズとして利用されています。例えば、以下のようなケース。
ちなみに、klass->iv_index_tbl->num_entriesが8だとifの中は実行されません。必要にならない限りメモリを割り当てないという方針のようです*4。
if (RBASIC(obj)->flags & ROBJECT_EMBED) { newptr = ALLOC_N(VALUE, newsize); MEMCPY(newptr, ptr, VALUE, len); RBASIC(obj)->flags &= ~ROBJECT_EMBED; ROBJECT(obj)->as.heap.ptr = newptr; } else { REALLOC_N(ROBJECT(obj)->as.heap.ptr, VALUE, newsize); newptr = ROBJECT(obj)->as.heap.ptr; } for (; len < newsize; len++) newptr[len] = Qundef; ROBJECT(obj)->as.heap.len = newsize; } }
というわけで値の設定です。
ROBJECT_PTR(obj)[index] = val;
何で埋め込んでいるのか、は何となくわかると思います。では何故埋め込めるのでしょうか?別の言い方をすると何故埋め込む空間なんてあるのでしょうか?そこら辺はRHGの「2.3.3 構造体の隙間」を見るとわかります。要約すると「オブジェクト構造体用のメモリは全部のオブジェクト構造体をunionしたRVALUE単位で割り当てるので、あるオブジェクト構造体だけでかいと無駄が発生する」ということになります。この無駄な空間をどうせだから埋め込み用に使おうという発想なようです。
Ruby1.8とRuby1.9でのインスタンス変数の扱いをまとめると以下のようになります。
スピードはそんなに変わらない気がします。メモリ効率はRuby1.9の方がかなりいいと思います。不真面目なのでベンチマークとか取ったりはしませんが。*5
「ところで何故埋め込めるのか」の部分は第1回 RHGの逆襲に参加することで知りました。このような会を開いてくださったYuguiさん、会場を用意してくださったよしおかさん、ささださん、その他多くの方に感謝します。