メジャーGCを減らす作戦

RGenGCやmrubyのシンプルな2世代管理の世代別GCでは、新世代オブジェクトはマイナーGCで1回マークされるだけで旧世代オブジェクトになる。この方式の利点は処理が軽いことで、欠点はマイナーGC実行時にタイミング悪く参照されていた一時オブジェクトが長寿命化することである。
たとえばGC_PROFILEを定義してビルドしたmrubyでao-render.rbを動かして出力から数えるとマイナーGCが10261回、メジャーGCが83回実行されたことがわかる。マイナーGCが動作するたびに少しずつオブジェクトが増えていき、閾値を超えるとメジャーGCが動作する。マイナーGCが動作したときに増えたオブジェクト数が、そのとき参照していた一時オブジェクトの数である。ao-render.rbは処理中に最後まで生き残るほど長寿命なオブジェクトを生成しないので、マイナーGCで確実に一時オブジェクトが回収されればメジャーGCはほとんど動作しないと考えられる。

世の中にはGCを生き残った回数をカウントして、一定数を超えたら長寿命化するというGCもあるらしく、そのような実装をしていれば無駄な長寿命オブジェクトを極力減らすことができるだろう。ただし、カウント処理にそれなりに負荷がかかる。
つまり、なんらかの処理の負荷を追加してオブジェクトを回収するようにしたとして、得られる利点はメジャーGCの回数削減であり、ao-render.rbで言うなら120回のマイナーGCに対して1回のメジャーGCをなくすことができるという話で、増える処理がメジャーGCの1/120の負荷でないといけないということになる。
ao-renderは保持オブジェクト数は少ないプログラムで、メジャーGCの負荷はあんまり高くなく回数も少ないと想像できるので、ao-renderをベンチマークとしてカウント処理を作るとおそらくまったくお話にならないパフォーマンスになる。実際、前処理部分だけ作って動かしてみたところao-renderが1.3倍ぐらい遅くなってしまったのでたぶんそんな感じだ。
そもそもmrubyはCRubyのような重量級コードを動かしたりしないだろうから、下手なものを作っても遅くなるだけである。逆にメジャーGCが頻繁に発生したりやたらと時間がかかるような場合は、それなりの仕掛けを導入する価値がある。このへんは難しい判断である。

ということで、考えていた仕掛けをメモっておく。
なるべく軽い処理ということでカウントを取ったりせず、フラグ管理で2回目のマークでオブジェクトを長寿命化しようと考えていた。オブジェクトにdmark_flgとかそんなフラグを1bit追加しておいて、初期状態ではリセット、1回マークしたらセットして白に戻す。フラグがセットされていたら黒のままにする。というのが概要となる。
フラグの遷移と白に戻す処理はマーク実行中にやるとやばいので、スイープが終わってからやる。対象は今回のマイナーGCで捉えた新オブジェクトで、つまりマークされたオブジェクト達である。現状のgray_listはスタック方式でマークしたら捨てていってしまうから、これをキュー方式に変更して先頭のポインタを保存し、フラグ処理の際に使いまわすようにしていた。実験した限りではこのキュー方式化が重かった。もう少し考えないといけない部分である。
あとは、オブジェクトを白に戻すと旧世代から参照されていた場合に次のマイナーGCでマークされないので、variable_gray_listも旧として1回分残し、新旧両方を処理する必要がある。こっちは1回目の場合には辿っても灰色のままにしておいて、2回目で黒にする。1回目を辿ったあとでライトバリアされた場合は旧から新のリストに移動する。実際には単方向リスト間の移動は大変なのでフラグを立てて最後に新に追加するとかそんな感じになる。
まだこまごまと実装レベルで変更はあるのだが端折って、とにかくこれでao-render.rbでのメジャーGCはほぼ発生しなくなるだろう。と思う。ただし、上のほうに書いた理由で現状パフォーマンスアップするような実装はちょっと難しそうだ。少なくとも俺には。