RGenGCに対応する方法

Ruby2.1.0-preview1は出たがREADME.EXT.jaを見ても書いてないので調べたことをここに書いておく。正式なマニュアルじゃないし検証してるわけでもないから豪快に間違っているかもしれん。

■はじめに
RGenGCは今までの拡張ライブラリのコードそのままで動作するように作られていて、何も考えずにコンパイルすれば普通に動く。2.1用に拡張ライブラリをコンパイルして動かない場合、よっぽど特殊なことをしていない限りはRGenGC以外のところに原因があると考えてよい。そのぐらい互換性に気を使われている。
なので特別に対応する必要は無いのだが、対応すればGCのパフォーマンスが上がるし、Shady化の処理を省く効果もあったりしてさらに速くなるかもしれない。速度を売りにした拡張ライブラリは対応しておいて損は無いだろう。
RGenGCの対応には大きくわけて以下の2つのパターンがある。両方やるのが一番よい。
RubyのArrayインスタンスがShady化しないようにする。
・自作クラスのインスタンスがShady化しないようにする。
それぞれの具体的な方法を書いておく。

■Arrayについて
RGenGCは世代別GCだから、旧世代オブジェクトに分類されたものはマイナーGCではマークしない。この動作はマーク対象を減らす効果があり、スループットを向上させる。ただし、旧世代オブジェクトが持つ別のオブジェクトへの参照が変更された場合に、新しいオブジェクトがマークされないことになるので、旧世代オブジェクトの書き換えを検出して特別にマークする必要がある。これは自動ではできないので、ライトバリアという仕掛けでRGenGCに変更を通知することになる。Rubyの組込クラスはこの処理が入っているわけだが、Arrayについては例外がある。
いままでは拡張ライブラリではRARRAY_PTRというマクロを使って配列の先頭のポインタを取得していた。C側でポインタを取り出すとそれをどこに保存しているかもわからず、いつ書き換えられるかがわからないため、RGenGCではそれが常に書き換えられていると仮定する。この「常に書き換えられていると仮定する」状態に置かれたオブジェクトはShady属性と呼ばれ、GC的には非効率な方法で管理される。
この状態を避けるため、Ruby2.1では新マクロRARRAY_AREF/RARRAY_ASETが定義された。もうちょい他にもあるがとりあえずこの2つを覚えておけばよい。それぞれ配列の値を取得/設定できる。
手元のDXRuby1.5.7devでは2.0以前でもコンパイルできるようにマクロを定義していて、それを見たらなんとなく使い方がわかるんじゃなかろうか。

#ifndef RARRAY_AREF
#  define RARRAY_AREF(a, i) (RARRAY_PTR(a)[i])
#endif

#ifndef RARRAY_ASET
#  define RARRAY_ASET(a, i, v) (RARRAY_PTR(a)[i] = v)
#endif

それぞれRARRAY_PTRをこのように使うような場合に使うものである。Ruby2.1でこのマクロを使うとライトバリアが適切に処理されてArrayオブジェクトがShady属性に堕ちなくなってGCのパフォーマンスが上がる。配列の先頭ポインタを保持しちゃってるライブラリはそもそも設計が間違ってると思うので(配列の先頭ポインタは変わる可能性があるので)修正したほうがよい。
特にRARRAY_PTRではShady化の処理が入るがRARRAY_AREFでは何もしないため中身を覗くだけならこっちのほうが速い。
また、Ruby1.9以降のArrayは要素が3つ以下だと外部にメモリを取らずにRubyオブジェクト内部に格納するため、RARRAY_PTRすると内部で判定式が実行されていて、実行速度を気にするライブラリではこれを回避するためにポインタを一時的に取得することがある。こういう場合用のマクロもあるようなので調べてみるとよい。たぶんRARRAY_PTR_USEとかがそういうものなんじゃないかと思うがよくわかってない。

■自作クラスについて
自分で作ったクラスでC構造体を持つものは、デフォルトではインスタンスを作った瞬間にShady化される。互換性のためには当然だ。DXRubyは1.8時代の拡張ライブラリであるから、README.EXT.jaに書いてあるData_Wrap_Structを使っているが、これを使うともれなくShady化されるようだ。
Shady化されないWB_PROTECTEDなオブジェクトを生成するにはTypedData_Make_Structを使う。具体的な利用方法はRuby2.1.0-preview1のiseq.cを見るとよい。これを使えばFL_WB_PROTECTEDフラグを立てつつマーク関数とフリー関数を定義することができる。また、オブジェクトのサイズを返す関数も指定できるが、これはobjectspaceがサイズを知るときに呼ばれるようだ。無くてもいいがどっちかというとあったほうがよい。
WB_PROTECTEDなオブジェクトはRGenGCからライトバリアが適切に入っていて信頼できるとみなされるため、そのオブジェクトが他のオブジェクトを参照するC構造体を持っていた場合、それを書き換えたときにライトバリアしてやる必要がある。しないとGCでまずいことになって運が悪いとコケる。テストで見つからない可能性もあって、リリースしてから見つかったりするかもしれないし、原因がわかりにくいバグになるだろうから、ものすごく気を使ってライトバリアを挿入する必要がある。そこまでする根性があるならFL_WB_PROTECTEDフラグを立てればGCのパフォーマンスは良くなる。
ライトバリアするにはOBJ_WRITEマクロを使う。これもまあ、使い方はiseq.cを見るとよい。ruby.hでマクロの定義を見てみるのもよいだろう。OBJ_WRITEする必要が無い(C構造体にVALUEを持たない)なら自作クラスのインスタンスはWB_PROTECTEDにしておくべきだ。
また、書き込む値がFixnumやQtrue、Qfalse、Qnilといった即値固定の場合もOBJ_WRITEする必要は無い。新しい参照先にオブジェクトが存在していなければ再マークしなくても問題ないからだ。このへんの最適化はRuby本体ではされているが、拡張ライブラリレベルでやるほど効果のあるものでもなさそうだからどうでもいいかもしれない。やたら要素数が多い配列の一部を書き換えるとかならやっておくと効果はありそうだ。

■おしまい
RGenGCはライトバリアの負荷を受け入れつつGCの処理を削って全体のスループットを上げる仕掛けである。DXRubyはアクションゲーム用のライブラリで、その特性からRGenGCの恩恵は(1フレームに1回マイナーGCが動くとかいう状態じゃない限り)普通のライブラリよりも少ないだろうと考えているわけだが、普通のライブラリはRGenGCが嬉しいと思うので、RGenGCへの対応方法は知っておいて損は無い。
いずれ誰かがちゃんとしたマニュアルを書いてくれるんじゃないかと。