Gauche:Gtkとメモリ管理

Gauche:Gtkとメモリ管理


クロージャを介した循環参照問題

Shiro (2002/11/27 17:50:57 PST): さかいさんとこ で指摘された問題なのだが、現在のGauche-gtkの実装ではメモリリークが生じる場合がある。

具体的には次のようなコード:

  (let ((button (gtk-button-new)))
    (g-signal-connect button "clicked" (lambda (w) (action button)))

で、buttonが指すオブジェクトと lambdaで作られるクロージャが解放されない可能性がある。

コールバックのメモリ管理

問題を整理するために、Gauche-gtkのメモリ管理をまとめておく。

(a) SchemeからGtkへのポインタ:

(b) GtkからSchemeへのポインタ:

さてここで、問題のコードに戻る。

図解すると次の通り。

              Scheme World               .         GTk World
                                         .
                                         .
                      ScmGObject         .
                /--->+------------+      .
   closed env   |    | SCM_HEADER |      .        GObject
                |    |------------| (a)  .        +----------+
    +-------+   |    |  gobject  ---------------->|          |
 /->|       ----/    +------------+      .        :          :
 |  |       |                            . (b)    |          |
 |  |       |                          /-----------   data   |
 |  |       |                          | .        + ---------+
 |  +-------+                          | .             :
 |                                     | .             :
 |                    ScmClosure       | .             : destroyNotify
 |                   +------------+    | .             :
 |                   | SCM_HEADER |<-+-/ .             :
 |     (c)           |------------|  |   .             :
 \---------------------   env     |  |   .             :
                     +------------+  |   .             :
                                     |(b')             :
                                     |   .             :
                      protected      |                 :
                      callback       |<_................
                      table          |   
                     +------------+  |   .
                     |            |  |   .
                     :            :  |   .
                     |          -----/   .
                     :            :      .
                     :            :      .

現時点での(不完全な)対応

Gtkオブジェクトへの参照が環境に閉じこまれないように気をつかってやる ことは出来る:

  (let ()
    (define (button-action w) (action w))

    (let ((button (gtk-button-new)))
      (g-signal-connect button "clicked" button-action))
    )

が、これではクロージャが自由に使えないのであまり現実的ではない。

ただ、頻繁にwidgetのdestroy/constructを繰り返すプログラムならともかく、 一度widget hierarchyを構築してそれを最後まで使い続けるような プログラムなら、循環参照は問題にならないとも言える。

将来、コンパイラをもっと賢くして、 必要以上に環境を閉じ込まないようにすれば、 以下のコードは循環参照を引き起こさないようにできる。

  (let ((button (gtk-button-new)))
    (g-signal-connect button "clicked" (lambda (w) (action w)))

どちらかのGCにフックが必要

問題の構造がGCされる条件は、

という両方の状態を満たすことである。 (ScmGObject⇔GObjectが1対1対応であることは保証されている)

ところで、現状では(1)を判断するにはScheme側GCでmark phaseを通すしか 方法が無い。一方、(2)を判断するにはGtk側でrefcountを0にするしかない。 いずれも、片方の条件が判明した時点で構造は回収に回されてしまうので、 その時点でもう片方が満たされていないため「待った」をかけることが出来ない。

現在の実装は回収に回されることを防ぐために、相互参照によって 条件(1)も(2)も決して満たされないようにしているわけだ。

「待った」をかけるためには、少なくともどちらかのGCメカニズムに手を加える必要がある。

Scheme側に手を加えるとすれば、mark phaseが終った時点で回収予定の ScmGObjectから指されているGObjectのrefcountを調べ、それが1ならば そのまま回収、しかし2以上ならばScheme側構造を再マークする、という パスが必要になる。 この再マークによっていもづる式に救済されるオブジェクトがあるだろうから、 マークは全部やりなおさなくちゃならない。

Gtk側に手を加えるとすれば、ref/unrefをフックして、 「ScmGObjectから指されていて、refcountが2以上である」GObjectを テーブルに登録しておき、Scheme側のGCの時にスキャンされるようにしておく。

待てよ、こういう方法もあるな。

これならScheme側GCの少しの改造で済むか?

GObjectからのみ参照されるScmGObject

GtkWidgetの派生クラスをSchemeから定義できるようにして 少しいじっていたら別の問題に気づいた。

ScmGObjectとGObjectの1対1対応を保証するため、 GObjectを指すScmGObjectが作成されると、GObjectのqdataに作成された ScmGObjectへのポインタを持つようにしている。

ところでGObjectのqdataはSchemeのGCからは見えない。 従って、

という場合に、ScmGObjectが回収されてしまうのだ。 qdataにあったポインタはScmGObjectのファイナライザにより削除される。

派生クラスを作らない場合はScheme側オブジェクトが 回収されても障害は目立たなかった。問題のGObjectを Scheme側で取り出す際に自動的に新たなScmGObjectが作られていたからだ。 (それでも、Scheme側のみに保存されているプロパティ等が失われていたのだが)。

しかし派生クラスを作っていると、サブクラスのスロットはScheme側で アロケートされているので、Scheme側オブジェクトが回収されてしまっては困る。

GMemVTableで解決なるか…と思ったが

どっかにフックかけられないかとGtkとGlibをつらつら眺めてたら、 Glibはメモリアロケータをすげ替えられることを今更ながらに知った。 g_mem_set_vtable()でユーザが用意したmalloc等を呼ぶように出来る。 ならこいつにGC_malloc等を渡してやれば、GtkからSchemeオブジェクトへの ポインタがScheme GCに見えるようになるからイケるんではないか。

と、喜び勇んでいろいろやってみたのだが、うーん、うまくいかない。

他のシステムではどうやってるのだ?

stklos

うーむ、GtkObjectは明示的にScheme側からdestroyされない限り 回収されないように見えるのだが…それにSchemeがGtkObjectへのポインタを 得た時にref/sinkしてないので、gtk側に既にあるGtkObjectを Scheme側で取り出して、その後でgtk側でunrefされるとまずいんじゃないのかなあ。

また、GtkObjectのdataにSchemeへのポインタをしまっているが、 別にprotectしている様子は無い。いいのかな。 glibのmallocをGC_mallocに置き換えているわけでもなさそうだし…

bigloo

Gtkへのバインディングはbigloo-lib中にある。

g-object-ref/unrefをSchemeレベルにエクスポートしている。 Scheme側で責任持ってref/unrefしろってことかいな。 Gtk側に渡したポインタをprotectするコードも見当たらない。 mallocは置き換えていないようだし。うーむ、謎。

なお、Biglooはデフォルトではfinalizerを使っていない。

guile-gtk

guileはモジュールがたくさんあってあちこちに分散しててよくわからない。 http://www.nongnu.org/guile-gtk/ が今のメインサイトなのかな。 あんまり開発が活発ではないようだけれど…

PyGTK

http://www.daa.com.au/~james/pygtk/

普通にdestroy callbackでunref。Gtk側に渡したPython objectも同じ。 python本体もrefcountingだし、循環参照は仕方ないってことか。 あれ、python 2.xになってmark-sweep GCも入ったんだっけ。 そこらへんをどうしてるかはわからない。

Gtk#

http://gtk-sharp.sourceforge.net/

Java-GNOME

http://java-gnome.sourceforge.net/

Hacking Boehm GC

Boehm GCもアプリケーション側がmark procedureを指定できるAPIが一応存在する。 一応というのは、アプリケーションレイヤではなく言語のランタイムルーチン等、 より低レベルのレイヤから使われることを想定したAPIだから。 mark procedure中でGCの内部情報にアクセスする必要があるし。 基本的には、「もともとbrute forceなconservertive pointer findingでも 動くけれど、もう少し賢くしてGCの効率を上げたい」というチューニング目的で 用意された感じだ。

従って、guile-gtkの方法を直接載せられるかどうかには疑問がある。 mark procedureはあまり多くのメモリページに触らないことを期待されているし、 incremental collectionのためにあまり長い作業をしないことも期待されている。

ま、とりあえず、gcのソースを読みながらごにょごにょいじってみよう。

Boehm GCではpointer scanningのポリシーが異なるアロケータをいくつも持てる。 ソース中では各アロケータを "kind" と称している。GC_malloc_atomicとか GC_malloc_uncollectableとかは別々のkindとして実装されているわけだ。 また、オブジェクト中のポインタの在処をビットマップで指定できるtypedとか、 オブジェクトクラス毎にmark procedureを持てるgcj用のkindなんかが オプションで用意されている。

各kindの情報はstruct obj_kindで記述される (gc/include/private/gc_priv.h)。 MAXOBJKINDS分のstruct obj_kindのアレイがグローバルに用意されており、 それ以上のkindを持つにはリコンパイルが必要。このことからもkindは 無闇に増やすことは想定されていないことがわかる。

アロケータそのものは全てのkindで共通。特に、小さなオブジェクトは ページ単位で確保されたプールからfreelistを使ってアロケートされるので、 そのfreelist arrayへのポインタはkindを作る側で用意しなければならない。

そのkindのオブジェクトをどうマークしたら良いかという情報は obj_kind.ok_descriptorにエンコードされる。(gc/include/gc_mark.h)

  1. オブジェクトの長さ+固定オフセットの分はスキャン
  2. スキャンすべきワードをビットマップで指定
  3. ユーザ定義のmark procedureを呼ぶ
  4. オブジェクト毎に指定オフセット位置にマークディスクリプタを保存

mark procedureを呼ぶ場合、procedure自体は配列GC_mark_procs[]に登録 されていなければならない。その配列のインデックスでprocedureを指定する。

Tag: GC

More ...