アリーナのかいしんのいちげき

mrubyにはmrb_gc_arena_save()/mrb_gc_arena_restore()という関数があって、これを使わずにCでオブジェクトを作りまくるとエラーでコケる。この件について作者のMatzが直々に日記を書いておられる。(http://www.rubyist.net/~matz/20130731.html#p01)
mrubyとC言語GCの問題点とその解決策を説明してくれているのだが、これを読んでもなんとなく微妙にわからない。という人は多いのではないかと思う。mrubyのGCを読んだことある人ならわかるんだろうけども。
で、俺も何か書いてみようと思ったわけだ。余計わからなくなるかもしれないが。しかしmrubyのarenaまわりのAPIはかなり内臓が飛び出したような設計だと思うので、使う人がGCとかオブジェクト管理を少し知らないといけないんじゃないかと。

■さわり
まず前提知識として、そもそもarenaとはなんぞや、という話なのだが、とりあえずGCが参照するルートオブジェクトのひとつである。mrubyのVM内で使われているオブジェクトを辿るのに、ルートオブジェクトから参照されているものすべてを辿ればOK、というのはmark&sweepアルゴリズムの考え方だが、そこにarenaが存在するということは、arenaに入っているものはそれ以外のルートオブジェクトから辿れない可能性がある、ということを意味する。そういうものを入れる場所ということだ。
じゃあ、どういうオブジェクトがarena以外から辿れないのかと言うと、ズバリ、Cで生成してRubyのコードから参照できない状態になっているオブジェクトである。mrb_class_new_instance()を呼んで返ってきた新規オブジェクトはローカル変数に入れたりするが、その状態がそれだ。Cのローカル変数はマシンスタックに入っているのでmrubyのGCはそれを参照できない、という話に関してはMatzにっきに書いてあったのでそっちを参照のこと。

■arenaとは
上記のような状態でGCに回収されると困るオブジェクトを入れる場所、ということで、100個分の領域を確保したスタック状のメモリらしい。arenaにデータを入れるタイミングはgc.cの中のmrb_obj_alloc()で呼んでるgc_protect()で、つまり新規に生成したオブジェクトはとりあえずすべてarenaに入れるようになっている。arenaにオブジェクトを入れるとスタックポインタが1つ進むので100個などすぐに溢れる。溢れたときにarena overflow errorが出るという寸法だ。なのでこのポインタをどこかで戻さなければならない。
VM上では、Cの関数を呼び出した後で、C側でオブジェクトを生成された場合に備えて、arenaのスタックポインタを呼ぶ前の状態に戻すという処理をしているので、rubyからCの関数を呼んだ場合は関数内で大量のオブジェクトを作ってさえいなければ問題は発生しない。
すなわちmrb_gc_arena_save()/mrb_gc_arena_restore()を使うタイミングは、この「C関数内で大量にオブジェクトを作ってarenaが溢れてしまうとき」ということである。

■mrb_gc_arena_save()/mrb_gc_arena_restore()はどう使うのが正しいのか
この関数はいったい何をするものなのか、が、まず問題である。簡単に言うと、mrb_gc_arena_save()がいまのarenaのスタックポインタを返してきて、mrb_gc_arena_restore()が引数で渡した場所にスタックポインタを移動する。ようするに、最初にmrb_gc_arena_save()を呼んでおいて、オブジェクトを生成しまくりつつ、arenaが溢れる前にmrb_gc_arena_restore()を呼んで強引にスタックポインタを引き戻す、というのが正しい使い方である。ただし、mrb_gc_arena_save()してから生成した中に残しておきたいオブジェクトがあった場合、arena以外のルートオブジェクトから参照できる状態にしておかないと、GCに回収されてしまう。ここが要注意ポイントだ。
そのようなオブジェクトはmrb_gc_arena_restore()を呼んだあとですぐにmrb_gc_protect()を呼び出してarenaに再登録して保護することになるが、ここまで読んできたらわかるように、mrb_gc_protect()で保護するオブジェクトはarena内に入るので、これも含めて100個を超えるとarena overflow errorになる。
ちなみに、mrb_gc_arena_restore()を呼んだあとで「すぐに」mrb_gc_protect()を呼び出すというのは、この間にmrubyオブジェクトの生成が挟まるとそのタイミングでGCが動いてmrb_gc_protect()を呼ぶ前に回収される可能性があるからだ。こういうところはCRubyでもハマりどころなので十分気をつけよう。mrubyではmrb_mallocの中でメモリ不足時にGCを動かすこともありえるので、たとえばmrb_gc_protect()をせずにインスタンス変数に入れればいいじゃーんって思っても、インスタンス変数の領域を確保するためにGCが動いて回収されてしまうという極めて珍しいバグが発生することも考えられる。
安全なやり方としては、mrb_gc_arena_restore()を呼ぶ前に配列やインスタンス変数などに格納するか、mrb_gc_arena_restore()を呼んだあとすぐにarenaの数に気をつけつつmrb_gc_protect()するということになるだろう。

■おわり
mrubyのarenaはCから扱おうとした場合に一番厄介な部分じゃないかと思っている。もうちょっとなんとかならんかなーとか考えてはみるものの、そんな革新的なアイデアが簡単に出るもんではなさそうだ。Cができる人は考えてみてもらえるとよいと思う。ナイスな仕掛けが作れればみんなが幸せになれる。