RGenGCとC拡張ライブラリの関係
Ruby2.1のRGenGCについて対応などを考えていたので個人的まとめ。
■世代別GCとC拡張ライブラリの相性の悪さ
RGenGCは世代別GCである。通常はRubyシステムによって適切に制御されたライトバリアと世代別管理されたGCによって効率よく処理されるが、C拡張ライブラリが関わるとライトバリアの制御が適切に行われない。C拡張ライブラリを書き換えてライトバリアのコードを挿入すればいいわけだが、数が多かったりメンテナ不在だったりするわけで、この方法で対応するのは現実的ではない。
RubyにおいてC拡張ライブラリがライトバリアと相性悪い部分は以下の2点である。
・CのコードはRubyオブジェクトの内部情報を取り出して保持できる。
・C実装のクラスはRubyオブジェクト(VALUE型)を保持するC構造体を持つことができる。
たとえばRARRAY_PTRで配列のポインタを取り出して保持した場合、既存のC拡張ライブラリのソースはライトバリアなどしないので、内容を書き換えてもRubyシステムはそれを検出することができない。また、C実装のクラスのインスタンス用構造体はRubyオブジェクトを保持できるが、どこかでそれを書き換えたとしてもRubyシステムはそれを検出することができない。
従って既存のC拡張ライブラリがある限り世代別GCを使うことは難しい。難しかった。
■Shadyオブジェクトという仕掛け
RGenGCでは通常の世代別GCアルゴリズムに加え、Shadyオブジェクトという概念を追加している。GCの管理外、日の当たらないところに存在するオブジェクトである。C拡張ライブラリから従来のマクロRARRAY_PTRでポインタを取得した場合、その配列はShady属性になる。また、未修正のC拡張ライブラリで定義されたクラスのインスタンスもShadyになる。GCに適切に管理されるRuby組み込みクラスのオブジェクトはSunny属性(いまはNormalだったかnon-shadyだったか)になる。
世代別GCではメジャーGCは全体を探索するが、マイナーGCでは新規作成されたオブジェクトのうちルートから辿れるものとライトバリアされたものをマークし、残ったものがスイープされる。RGenGCでは新規作成されたオブジェクトはルートオブジェクトから探索され、マークされたら旧世代オブジェクトに移行する。ライトバリアされたオブジェクトはRememberSetというルートオブジェクトに登録され、マーク時に探索される。マークされるとRememberSetから削除され、旧世代オブジェクトに移行する。このあたりの世代別GCの動作はmrubyと同様であり、普通に2世代管理の世代別GCである。
Shadyオブジェクトは生成時からすでにShadyであるものと、途中でShadyになるものの2種類がある。Cクラスのインスタンスは生成時からShadyであり、それらは普通に新規オブジェクトとして扱われるが、マーク時に旧世代オブジェクトからの参照を検出するとRememberSetに登録される。通常旧世代からの参照はライトバリアが無い限りは探索する必要が無いのだが、Shadyオブジェクトは自身の内容を書き換えてもライトバリアが発生しないので、常時ライトバリアで保護された状態にするのである。常時なのでマークされてもRememberSetから削除されない。メジャーGCが動作するまでライトバリアされ続けるので回収もされない。
また、Arrayは生成時はSunny属性なのだが、RARRAY_PTRなどでポインタを取得した場合、Shadyになる。参照元がわからないのでShady化したタイミングでRememberSetに登録され、以下同様となる。
■C拡張ライブラリの動作
さて、このような仕掛けで従来のC拡張ライブラリとの互換性を確保するRuby2.1だが、たとえばDXRubyなどをそのままコンパイルして使った場合に何が起こるかを考えてみよう。
オブジェクトの寿命を短/中/長と3段階に分けたとすると、DXRubyを使ったゲーム的なプログラムで考えれば、Vectorオブジェクトは短寿命で、Spriteが中寿命で、Imageは長寿命ということになりそうだ。これらはすべてRGenGCではShady属性という扱いになる。また、内部でたとえば衝突判定時などで生成している配列についてもRARRAY_PTRを使っているのでShady化する。ShadyオブジェクトはマイナーGCではすべて毎回マークされ、メジャーGCが発生するまで回収されない。と思う。間違ってるかもしれないが。
まあ、そうであると仮定すると、マイナーGCは無駄に重くなるし、メジャーGCの発生率も上がるということになる。対応するとライトバリアの処理が増えるので通常の負荷が微妙に上がるのだろうが、マイナーGCは軽くなるしメジャーGCは減るのでGC発生時に処理落ちするパターンは減る。
そもそも世代別GCはライトバリアが増える代わりにGCの負荷が減って総合的にスループット改善を狙うアルゴリズムであるから、1フレーム内の処理に限定して考えるアクション系ゲームとは重視する部分が違う。1フレームに間に合っていればどれだけ速くても関係なく待ち時間ができるし、もし処理落ちはまったく許されないとしたなら、ライトバリアの負荷+メジャーGCのフレームは従来よりも条件が悪化するわけで、善し悪しとも言える。そういう意味ではメジャーGCにインクリメンタルGCなりコンカレントGCなりが来てくれると状況はサイコーに改善する。まあ、CRubyはゲーム向けではないのでゲーム開発系ユーザが特殊すぎて発言力は皆無なのだが。
■結局のところ
DXRubyはタイミングを見てRGenGCに対応する方向で考えている。実際にはPreview版あたりが出てから対応してみて動かしてみてパフォーマンスがどうなるかというのが重要な話であって、いま決定するわけではない。
コードを眺めている限りだとあまり心配することも無いのかなという感じではある。