SyntaxHighlighter

2012年4月15日日曜日

Ruby の Garbage Collection に関するメモ

Exerb でのバグ報告に関連して、Ruby の Garbage Collection について調べていたときのメモです。

Ruby の EXE 化

Ruby 勉強会の準備をしていて、「Windows 上での Ruby の EXE 化」についてまとめていました。
私は Ruby 1.9.3 では Ocra を使っていますが、Ruby 1.8.7 の頃は Exerb を使っていました。

どちらもとてもよいツールでお世話になっていますが、どちらかというとレシピファイルで同梱ファイルを指定できる Exerb の方が好きです。
ただし、Exerb は Ruby 1.9 には対応していません。
もともとの作者の Yuya さんは、今は開発に関わっていないらしく、このまま 1.9 系列の開発はされないのかもしれません。
ちょっと残念です。

Exerb で報告されていた挙動

2月ぐらいに、Integer#to_s でごくまれに変な文字列になるという報告がされていました。
で、今回、勉強会の資料を作るついでに気になったので (というか、重大なバグがツールに含まれていると勉強会でおすすめしづらいので) 調べてみました。

たとえば、以下のようなソースで、ごくまれに変な文字列が表示されます。

原因

原因を調べるために、何ヶ月かぶりに Windows を立ち上げて調べてみました。

原因は、Bignum クラスのメソッド to_s 内で、テンポラリに作られた Bignum オブジェクトが、意図せず Garbage Collection で解放されてしまうためでした。

なお、これは Ruby 1.8.7 での話です。Ruby 1.9 では、このあたりがもう少し高速なアルゴリズムも併用するように変わった関係で、発生しません。

bignum.crb_big2str0 関数に以下のようなところがあります。

ここでは、(1) で変換元の値 x をコピーして、その中の実際に数値を保持している配列のアドレスを (2) でポインタ ds に取り出しています。
その後、戻り値用の文字列オブジェクトを (3) で用意し、これ以降で変換した後の文字列を生成しています。

障害が発生する場合は、(1) で生成したオブジェクトが (3) で解放されてしまっていました。
その結果、(3) 以降で t の中身である ds を参照しても、変な値しか残っていない状態になっていました。

Ruby の Garbage Collection

Ruby のスクリプトの中で new されたオブジェクトならともかく、C言語で書かれた Ruby ライブラリや、Ruby 自体のソースコードの中で生成されたオブジェクト (今回の t のようなもの) が、どうやって自動で解放されているのか、ずっと不思議でした。
オブジェクトの一覧は簡単に取得できるはずですが、どれを解放の対象とするかどうかの判断が難しいのではないかと思っていました。

WEB 上で資料を探してみると、少し古いですがRuby ソースコード完全解説の中で詳しく解説されていました。

こちらを読みながら、実際に現在のソースコードを見ていると、C言語ライブラリなどで生成されたオブジェクトは、スタック上に Ruby オブジェクトらしきものを探し、発見されればそれは Garbage Collection の対象としないという仕組みになっているようでした。

率直な感想としては、「そんな適当に判断して大丈夫なの」というものでしたが、Garbage Collection は、解放可能なものをすべて解放する必要はなく、解放すべきでないものを解放してしまわなければよいものなので、これでもうまくいくようです。

ちなみに、今回の障害は、上記ソースコードの tds が運悪くスタック上の同じ番地にとられてしまうと発生していました。
Exerb のコンパイルオプションだと、偶然そうなっていたようです。
その結果、(3) の時点では既にスタック上からは t そのものは消えており、Garbage Collection による解放の対象となってしまっていました。

修正

よって、ds を参照している間は、t もスタック上に残っているようにすれば、間違って解放されることもなくなります。

Ruby のソースコードでは、このためのマクロがあり、RB_GC_GUARD が定義されています。

これを使い、RB_GC_GUARD(t);ds を最後に参照している箇所より後に書けば、うまくいくようになります。

マクロ RB_GC_GUARD は、volatile 修飾子を使って、引数である t を強制的にその場で参照するようにするものです。
その結果、tds と同じメモリ番地に割り当てられるようなこともなくなり、きちんと t がスタック上に残るようになります。

というわけで、念の為に Ruby 本家の方も直してもらおうとチケットを書きかけましたが、Exerb のメーリングリストを見ておられたのか、Ruby コミッタの nobu さんが既に修正されていました。

nobu さん、ありがとうございます。
そして、Ruby 1.8.7 の ChangeLog に私の名前も入ることになりました。わーい。

0 件のコメント:

コメントを投稿