RでC APIを使う時、SEXPをGCから守る3つの方法

extendrの実装おかしくない?、というのを最近調べていて、理解が深まったのでメモっておきます。

GCとは?

Rにはgarbage collection(GC)があるので、使われなくなったオブジェクトは勝手に削除されます。 逆に言うと「使われているオブジェクト」であることを示さない限り、勝手に削除されてしまいます。

削除されると何が起こるか?

これです。削除されて解放済みのメモリを読みに行くとエラーになります。

通常のRのコードを書いているときは気にしなくて大丈夫ですが、RのC APIを使ってC/C++やRustでコードを書く時は気にする必要があります。

WRE

まずは、Writing R Extensionsの5.9.1 Handling the effects of garbage collectionを読みましょう。

PROTECT()

もっとも一般的なのは、PROTECT()を使う方法です。

If you create an R object in your C code, you must tell R that you are using the object by using the PROTECT macro on a pointer to the object.

具体例として以下のコードが掲載されています。

    SEXP ab;
    ab = PROTECT(allocVector(REALSXP, 2));
    REAL(ab)[0] = 123.45;
    REAL(ab)[1] = 67.89;

これは、PROTECT()は引数のSEXPオブジェクトをそのまま返すのでこうしていますが、別々の行に分けることもできます。

    SEXP ab;
    ab = allocVector(REALSXP, 2);
    PROTECT(ab);
    REAL(ab)[0] = 123.45;
    REAL(ab)[1] = 67.89;

ただし、このコードは不完全です。 PROTECT()されたままだと永遠にGCされないので、関数から抜ける前には必ずUNPROTECT()する必要があります。 つまり、こういう感じのコードになるはずです。

SEXP new_real2() {
    SEXP ab;
    ab = PROTECT(allocVector(REALSXP, 2));

    REAL(ab)[0] = 123.45;
    REAL(ab)[1] = 67.89;

    UNPROTECT(1);
    return ab;
}

ここで、PROTECT()の引数はSEXPである一方、UNPROTECT()の引数は数字になっていることに気付いたかと思います。 なんで?、と思いますが、WRE曰く、

The protection mechanism is stack-based, so UNPROTECT(n) unprotects the last n objects which were protected.

ということで、スタックベースなのでこういうインターフェースになっているみたいです。 たぶん、UNPROTECT(<SEXP>)にすると、そのSEXPを探すコストが発生して遅くなってしまうからなんだと思います。

つまり、「とりあえず戻り値は PROTECT() したままで、あとで UNPROTECT() しよ」みたいなことはできません。 関数を抜けてしまうともう追えなくなってしまうので、PROTECT() の回数とと同じだけ UNPROTECT() を呼び出して、 最後は何も PROTECT() されていない状態にする必要があります。もし回数が違っていると警告が出ます。

Pointer-protection balance. Calls to PROTECT and UNPROTECT should balance in each function. A function may only call UNPROTECT or REPROTECT on objects it has itself protected.

「in each function」とは書かれていますが、必ずしも1つの関数の中でバランスが取れている必要はないはずです。 具体的には、ユーザーが .Call() で呼ぶ粒度でバランスが取れていれば大丈夫なはずです。たぶん。

とはいえ、基本的には、関数はPROTECT()されていない状態の戻り値を返して、呼び出した側がPROTECT()する、という役割分担でいいはずです。 関数に渡す引数も、呼び出した側がPROTECT()することになっています。

Caller protection. It is the responsibility of the caller that all arguments passed to a function are protected and will stay protected for the whole execution of the callee.

Protecting return values. Any R objects returned from a function are unprotected (the callee must maintain pointer-protection balance), and hence should be protected immediately by the caller.

別のオブジェクトから参照

ただし、何でもかんでもとりあえずPROTECT()すればいいかというと、大量のSEXPを生成するときは、スタックが溢れる可能性があるよ、みたいなことが書かれています。

Be particularly aware of situations where a large number of objects are generated. The pointer protection stack has a fixed size (default 10,000) and can become full. It is not a good idea then to just PROTECT everything in sight and UNPROTECT several thousand objects at the end.

じゃあどうすればいいかというと、こう書かれています。

It will almost invariably be possible to either assign the objects as part of another object (which automatically protects them) or unprotect them immediately after use.

方法はふたつ、

  1. すぐに別のオブジェクトから参照されるようにする
  2. 使い終わったらすぐにUNPROTECT()する

このうち、後者は特に説明しなくてもわかると思うので、前者についてだけ書きます。 上でみたPROTECT()のコード例では、allocVector()したらすぐにPROTECT()していましたが、たとえばそのあとすぐに list(VECSXP)に入れてしまうのであれば PROTECT() は不要、みたいな話です。

また、WREには書かれていませんが、別のオブジェクトから参照されるようにする方法は、PROTECT()と違って解除を任意のタイミングに遅らせることができます。 cpp11ではこの仕組みを使ってSEXPをGCから守っています。extendrでも同じです。

ただし、任意のタイミングに遅らせることができる、と書きましたが、解除を忘れるともちろんメモリリークになります。 これはこれで頭の痛い問題ですね。

R_PreserveObject()

もうひとつ用意されている方法が、R_PreserveObject()という関数です。 これは、スタックベースのPROTECT()とは別の仕組みになっていて、一度R_PreserveObject()されたオブジェクトはR_ReleaseObject()されるまでGCから守られ続けます。

There is another way to avoid the effects of garbage collection: a call to R_PreserveObject adds an object to an internal list of objects not to be collects, and a subsequent call to R_ReleaseObject removes it from that list. This provides a way for objects which are not returned as part of R objects to be protected across calls to compiled code

ただし、この方法は効率が悪いので多用は厳禁、という注意書きがあります。

It is less efficient than the normal protection mechanism, and should be used sparingly.

効率が悪いというのはどういうことかというと、linked listになっていて、オブジェクトをリリースするときは全走査しないといけないので、特にこの方法で守られているオブジェクトが多い時はきつい、ということみたいです。Rcppでその問題が指摘されていました。

実用上は、cpp11のような方法を実装している場合はそちらを使うのがまずは選択肢です。 その場合、R_PreserveObject()は、そのオブジェクトを絶対に削除しない、もしくはまれにしか削除しない、という場合に使われるようです。 実際、cpp11もその起点となるオブジェクトはR_PreserveObject()で守られています(コード)。これはセッション中、絶対に削除されないものです。