Gauche:メモリリーク
Gaucheで書いたプログラムがメモリをリークしているのですが、これの原因調査は、やっぱりコードの怪しい部分を見ていく以外ないでしょうか?特別なテクニックはありますか?
- skimu: なぜメモリリークしているという結論に至ったかの過程を具体的に、できたら他の人が再現可能な形でGauche:Bugs なり、gauche-devel なりに報告しましょう。
- ちょっとしたデーモンを書いたのですが、それを動かしていると数日でメモリを使い果たして死んでしまうので、メモリリークと判断しました(というか確かにリークしている)。疑っているのはGaucheではなく自分のプログラムで、なにかのポインタが残ってしまっているのだと思うのですが、それを調べるのが大変で。これをデバッグするときに、たとえば現在の時点でアロケートされているオブジェクトの個数がクラスごとにわかったりすれば、どこらへんがおかしいのかあたりがつきますよね。その手の方法があったりすればいいなぁと思ったのでした。通るコードの量を減らしてみると、リークする早さはゆっくりになったものの、それでもじょじょにずっと増えていきます。Boehm GCを疑いたくはないけど関係あるのかなあ。もうちょっと調べて、できれば再現可能な形でまた報告します。
- び(2005/10/19 02:35:24 PDT): わたしがScheme初心者だった頃よくやらかしたのは、「末尾再帰でループを書いてるつもりで、実は末尾再帰になってなかった」というやつですかね。条件で枝分かれした先で再帰するようなコードで、特定ケースだけ末尾再帰にし損ねてたり。そうすると、ループが回ってある条件を満たすたびにちょっとずつメモリを食い潰していくことになって、なかなか気づかなかったものです。今でもたまにやります(笑)。
- Shiro(2005/10/19 03:20:10 PDT): どんなGCにも「癖」があって、
苦手なパターンにはまると問題が出る時があります。またGauche特有の
注意点もあります。対応はケースバイケースなので具体的なシチュエーションが
わからないと何とも言えないのですが、一般的に注意すべき点をあげておきます
(ちょっと「初心者」の枠からは外れるかもしれませんが…)
- Boehm GCのような保守的GCは、「ポインタのように見えるワード」を全てポインタと 看做すために、原理的にリークの可能性があります。「ヒープポインタのように見える ビットパターンを取るデータをたくさん格納したメモリブロックがたくさんアロケート されている」状況で問題が発生します。C拡張を書いている場合は、ポインタを格納 しないメモリブロックをATOMICでアロケートするようにして下さい。純Schemeで書いている 場合は、大きめの整数値をたくさん格納している巨大なベクタを使っていたら、 整数値部分だけuniform vectorに格納できないかどうか検討してみて下さい。
- Gaucheのクロージャは今のところ、外部の環境を全て、クロージャの中で使っているいないに
かかわらず掴んでいます。例えば以下のコードで作られるクロージャはsimple-valueしか
参照していませんが、tmpを含む環境を掴んでいるので、クロージャが開放されない限り
tmpの指すデータが開放されません。問題になることはまれだとは思うのですが。
なおこの問題は最適化を進めてゆく過程で改善される予定です。
(let* ((tmp (make-huge-data)) (simple-value (calculate-a-simple-value tmp))) (lambda () (do-something simple-value)))
- C拡張ライブラリを使っている場合、原理的にGCだけではメモリを回収できない ケースがあります(例: Gauche:Gtkとメモリ管理)。そういう場合は明示的に リソースを開放する必要があるかもしれません。
- 環境は何でしょうか。cygwinでリークすることがあるのでは? という報告が Gauche:Bugsに上がっていたと思います。 cygwinはunix環境のエミュレートのためにそうとう黒魔術っぽい技を使ってるみたい なんで、追うのが大変そうなんですが。
- Boehm GC側はSchemeのデータ構造を把握していないので、「クラス毎のアロケーション」 のような細かいデータは取れません(Boehm GCからは一回毎のアロケーションのワード数 しかわからない)が、環境変数GC_PRINT_STATSやGC_DUMP_REGULARLYを定義してgosh を走らせると色々報告してくれるので手がかりが得られるかもしれません。
- C拡張を書いていて、怪しい(開放されるべきなのに開放されてないのではと疑われる) データがある場合は、データアロケーション時にScm_GCSentinel()を噛ませてみて 下さい。するとそのデータがGCされる時に報告してくれます。
- みなさんありがとうございます。
- コードを単純化して変更してみたところ、Hans BoehmのBounding Space Usage of Conservative Garbage Collectorsで、an embarrassing failure scenarioとして例にでている通りのパターンが原因のようです。コードの中で連想リストをキューとして使っており、その連想リストには、リストの途中(末尾のこともある)にペアを挿入するという操作を行っています。このキューの先頭からペアを削除する(参照をひとつ進める)ときに、先頭のペアのcarとcdrに#fを代入するようにすると、メモリ使用量が増えなくなりました。たぶん図にすると次のようになっていたと思います。
本物の参照 | | +----+----+ +----+----+ +----+----+ +--|-+----+ +----+----+ | | --------->. | --------> | -------->. | ---------> | ---... +----+----+ +--|-+----+ +----+----+ +----+----+ +----+----+ | | 偽物の参照 |--------------| |-------------------------... 死んだオブジェクト 生きているオブジェクト |----------------------------| 死んでいるが、偽物の参照に回収 が妨げてられているオブジェクト
- 上のリストを構成するペアの1つが偽ポインタに参照されると、その後リストに挿入されるペアはすべて回収から外れてしまいます。つまりオブジェクトをキューに追加するとそれは回収されなくなります。これで際限なくメモリ使用量が増えていたのかと。
- util.queueは上記のリストと同様の構造なので、同様の問題が起こるのかもしれませんね。
- コードを単純化して変更してみたところ、Hans BoehmのBounding Space Usage of Conservative Garbage Collectorsで、an embarrassing failure scenarioとして例にでている通りのパターンが原因のようです。コードの中で連想リストをキューとして使っており、その連想リストには、リストの途中(末尾のこともある)にペアを挿入するという操作を行っています。このキューの先頭からペアを削除する(参照をひとつ進める)ときに、先頭のペアのcarとcdrに#fを代入するようにすると、メモリ使用量が増えなくなりました。たぶん図にすると次のようになっていたと思います。
- Shiro(2005/10/20 03:19:14 PDT): その論文は大いに参考になりました。 Weak GC-robustでないデータ構造については使う側で安全策を取っておくのが 良さそうですね。
Tag: GC