mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方がまだよくわからないので実験してみた

Matzがmrubyのarenaの使い方について書かれています。

Matzにっき(2013-07-31) _mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方

自分も時々arena_overflowになって悩まされていたのですが、いまいちわからないままあやしいところをmrb_gc_arena_save()とmrb_gc_arena_restore()で囲って、動けばいいや的なノリで切り抜けてました。

Matzの記事ではCで拡張(mrbgem)を書く場合を例に説明されてますが、自分はmrb_funcallなど、C側からmrubyを呼ぶ場合にどうすれば良いかもよくわかっていなかったので、いろいろ実験してみました。

arena_overflowとやらをおこしてみる

とりあえず何も考えずに適当なクラスのインスタンスを作りまくってみます。

#include <stdio.h>
#include "mruby.h"

// Foo#func: 引数なし、適当なStringを返す。以下ずっと中身は同じなので省略
mrb_value func(mrb_state *mrb,mrb_value self)
{
    printf("Foo#func called\n");
    mrb_value val = mrb_str_new_cstr(mrb, "value_created_in_func");
    return val;
}

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
    }
    
    mrb_close(mrb);
    return 0;
}

結果

$ ./arena
index = 0
index = 1
...
index = 97
index = 98

Abort trap: 6

index=98、つまり99個目のインスタンスを作ろうとして落ちました。あれ?100個分あるんじゃなかったの?98個しか出来ねえじゃんというのはあるのですが、そこは後で判明したのでとりあえずこのまま進みます。というか、以前はarena overflowとかメッセージが出たような気がするのですが・・。

mrb_gc_arena_save(), mrb_gc_arena_restore()でarena_overflowを防ぐ

で、噂のmrb_gc_arena_save()とmrb_gc_arena_restore()をつかってみます。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    mrb_close(mrb);
    return 0;

結果

$ ./arena
index = 0
index = 1
...
index = 108
index = 109

お〜、これでOKぽいですね。でも、これじゃ意味がありません。mrb_gc_arena_restore()で全部元に戻しちゃったら必要なオブジェクトが使えませんね。そういう必要なオブジェクトはmrb_gc_protect()で保護せよとのことなので、その通りにしてみます。

mrb_gc_protect()で必要なオブジェクトを保護

とりあえず、for文で作ったオブジェクト(Fooクラスのインスタンス)の最後だけは保護するということにしてみます。ついでにちゃんと使えるか確かめるために、funcというメソッドを呼び出します。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    //最後のインスタンスを保護して、使ってみる
    mrb_gc_protect(mrb, foo);
    mrb_funcall(mrb,last_foo, "func", 0, NULL);
    
    mrb_close(mrb);
    return 0;
}

結果

$ ./arena
index = 0
index = 1
...
index = 108
index = 109
Foo#func called

これで良さそうです。ですが、mrb_funcallでFoo#funcを呼び出すとStringが(mrb_valueで)返ってきますが、こいつは大丈夫なんでしょうか。こんどはFoo#funcを100回以上呼び出してみます。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    //最後のインスタンスを保護して、100回以上使ってみる
    mrb_gc_protect(mrb, foo);
    for (int i = 0; i < 110; i++){
        printf("index(func) = %d\n", i);
        mrb_value ret = mrb_funcall(mrb, foo, "func", 0, NULL );
    }
    
    mrb_close(mrb);
    return 0;
}

結果

...
index(func) = 47
Foo#func called
index(func) = 48
Foo#func called

Abort trap: 6

あ〜、またarena overflowっぽいです。100回の半分程度で落ちてますね。Foo#fooの中で文字列を作っているのと、mrb_funcallで値を戻すので2倍消費するのでしょうか?・・・。まぁとりあえずmrb_funcall周りもmrb_gc_arena_save()とmrb_gc_arena_restore()で囲ってやったらよさそうです。

mrb_funcall()周りをmrb_gc_arena_save()とmrb_gc_arena_restore()で囲ってarene_overflowを防ぐ

こんな感じでどうでしょう(疲れてきた。。。)

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    //最後のインスタンスを保護して、100回以上使ってみる
    mrb_gc_protect(mrb, foo);
    
    ai = mrb_gc_arena_save(mrb);
    for (int i = 0; i < 110; i++){
        printf("index(func) = %d\n", i);
        mrb_value ret = mrb_funcall(mrb, foo, "func", 0, NULL );
        mrb_gc_arena_restore(mrb, ai);
    }
    
    mrb_close(mrb);
    return 0;

結果

...
index(func) = 108
Foo#func called
index(func) = 109
Foo#func called

よしよし、これで良いんじゃないでしょうか?


・・・ただ気になるのは最初の時点で98個しかオブジェクトが作れなかったことです、どうも、クラスの登録とメソッドの登録もarena_indexを増加させてるんじゃないでしょうか?
なので、そこも囲ってみました。あとarena_indexの値も表示して確認してみます。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    int ai = mrb_gc_arena_save(mrb);
    printf("arena index before registering class = %d\n", ai);
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    printf("arena index after registering class = %d\n", mrb_gc_arena_save(mrb));
    
    mrb_gc_arena_restore(mrb, ai);
   
   //ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        //mrb_gc_arena_restore(mrb, ai);
    }
    ...

結果

$ ./arena
arena index before registering class = 0
arena index after registering class = 2
index = 0
index = 1
...
index = 99
index = 100

Abort trap: 6

やっぱりmrb_define_class()とmrb_define_method()もarenaを増加させていたようです。この辺周りの関数を呼び出す時も注意ですね。

mrbgemでクラスやメソッドを登録する際

今回はmrbgemではなく、普通にmainの中でmrubyにクラスやメソッドを登録したのですが、それはarena_indexを増やすのでした。
ということはmrbgemを書く場合にもクラス登録時にmrb_gc_arena_save()やmrb_gc_arena_restore()で囲ってやる必要があるのかと心配になったのですが、それよきにはからってくれるっぽいです。
以下はmrubyのビルド中に作られるmruby-sprintfというmrbgemの初期化コードです。

...
void GENERATED_TMP_mrb_mruby_sprintf_gem_init(mrb_state *mrb) {
  int ai = mrb_gc_arena_save(mrb);
  mrb_mruby_sprintf_gem_init(mrb);
  mrb_gc_arena_restore(mrb, ai);
}

このように、実際にクラス、メソッドを登録するmrb_mruby_sprintf_gem_init()周りはmrb_gc_arena_save()とmrb_gc_arena_restore()で囲まれてるんですね。なるほど。