メッセージ。 - クレクレ君第2弾 メモリ解放してホシス
# クレクレ君第2弾 メモリ解放してホシス
Gauche関連でついでにもう1つ。
GaucheでWebサーバーを作っているのですけど、コードのどこかでメモリリークしているような気がするんです(Gaucheが悪いんじゃなくて、まずは自分のコードを疑っとります)。
それで、コードをシェイプアップしてどこがまずいのか調べてるんですが、次のコードでもうすでに×な感じです。
テストの方法としては、
- 上記を実行してから
- 「ps auxw | grep gosh」などとしてメモリ使用量を確認
- 「ab -n 10000 http://localhost:8082/」(Apache Benchで10000回アクセス)
- 「ps auxw | grep gosh」などとしてメモリ使用量を確認
という感じです。
Zaurus上のGauche 0.8.3だけでなく、x86上のGauche 0.8.6でも確認しましたが、現象が出ているような気がしました。あるいは調べ方が間違ってるのかもしれません。継続して調べるつもりですが、いかんせん時間がなく、これから仕事の佳境に入ることもあって、ここで一旦クレクレ君することにしました。スミマセン。
GaucheでWebサーバーを作っているのですけど、コードのどこかでメモリリークしているような気がするんです(Gaucheが悪いんじゃなくて、まずは自分のコードを疑っとります)。
それで、コードをシェイプアップしてどこがまずいのか調べてるんですが、次のコードでもうすでに×な感じです。
#!/usr/bin/env gosh(use gauche.net)(let1 ssocket (make-server-socket 'inet 8082)(let loop ((csocket (socket-accept ssocket)))(let ((iport (socket-input-port csocket))(oport (socket-output-port csocket)))(read-line iport)(display "HTTP/1.0 200 OK\r\nContent-type:text/html\r\n\r\nO.K." oport)(socket-close csocket))(loop (socket-accept ssocket))))
テストの方法としては、
- 上記を実行してから
- 「ps auxw | grep gosh」などとしてメモリ使用量を確認
- 「ab -n 10000 http://localhost:8082/」(Apache Benchで10000回アクセス)
- 「ps auxw | grep gosh」などとしてメモリ使用量を確認
という感じです。
Zaurus上のGauche 0.8.3だけでなく、x86上のGauche 0.8.6でも確認しましたが、現象が出ているような気がしました。あるいは調べ方が間違ってるのかもしれません。継続して調べるつもりですが、いかんせん時間がなく、これから仕事の佳境に入ることもあって、ここで一旦クレクレ君することにしました。スミマセン。
Comment
# リークというか
この束縛が、let loop の再帰のたびに増えていってるんじゃないでしょうか?
iportとoportをもっと外側の方で用意して、内側では set! にしてみたらどうでしょう。
# set!はイヤイヤ(T_T)
リークじゃなくて、メモリの制御がGCに移っているだけじゃないかということですよね。
んー。まぁたしかにそうかもしれないんですが、いつまでたってもGCがメモリを解放してくれてない感じがしまして。
しばらく待っていても、psコマンドで見えるメモリ使用量が減らないんです。GCによるメモリ解放タイミングの問題かもしれませんが……。
とくにZaurus上で動かしていると、メモリが足りなくてほかのプロセスが強制終了されちゃうんですよねぇ……。
と、これはちょっと違う話かなぁ。
とりあえず、iportとoportの束縛がloopの中で増えている可能性はたしかにありますね。でも、set!はできるだけ使いたくないんです (T_T)
#
(begin
(use gauche.net)
(let1 ssocket (make-server-socket 'inet 8082)
(let loop ((csocket (socket-accept ssocket)))
(socket-close csocket)
(loop (socket-accept ssocket)))))
ではメモリ使用量が増えず、
(begin
(use gauche.net)
(let1 ssocket (make-server-socket 'inet 8082)
(let loop ((csocket (socket-accept ssocket)))
(socket-output-port csocket)
(socket-close csocket)
(loop (socket-accept ssocket)))))
ではメモリ使用量が増えました。
その場しのぎですが、手元の環境では以下のパッチが効きました。
--- ext/net/net.c.orig 2005-10-13 17:14:13.000000000 +0900
+++ ext/net/net.c 2006-03-28 17:33:57.000000000 +0900
@@ -135,6 +135,8 @@
closeSocket(s->fd);
s->fd = INVALID_SOCKET;
s->status = SCM_SOCKET_STATUS_CLOSED;
+ s->inPort = NULL;
+ s->outPort = NULL;
return SCM_TRUE;
}
# おお、ありがとうござ...
このコードだけを見る限りでは、変数のクリア漏れでアドレスっぽい値が残っていたので、GCが掃除対象と見なさなかった、という感じなんですかねぇ。いや、いい加減なこと書かないほうがよさそうだけど。
(cf. 一応、「ナンノコッチャ」な技術者さん向けに、http://homepage2.nifty.com/aito/gc/gc.htmlの「Boehm GCの原理」)
# ふーむ。
元コードにおいて、loopした時点でcsocket, iport, oportのローカル変数エリアはスタック上で上書きされるはずなんですよね。上のパッチが効くってことはソケットのファイナライザは呼ばれてる=ソケット自体はGCされてるってことですし…
output portにもファイナライザがついているんで、もしかするとファイナライザ付きオブジェクトの相互参照に絡んだ問題かもしれません
# なるほど
このあたりのコード、まだしばらく動かしますので
また何かありましたらご報告します。
# ちょっと調べました
ファイナライザがついてるオブジェクト、およびそこからたどれるオブジェクトは、一度ごみと判断されてもファイナライザを走らせるためにGCサイクル一回分、延命されます。
一方GCは、あるサイクルで回収したメモリが少なかった場合、メモリ領域を拡張します。
ということは、あるサイクルでごみになったオブジェクトのほとんどがファイナライザつきオブジェクト、およびそこからたどれるオブジェクトの場合、意味的にそれらはごみであるにもかかわらず、GCはメモリ領域の拡張を要求することになります。
今回のサンプルコードのように、ループ内でファイナライザリーチャブルなオブジェクトのみをがんがんアロケートしている場合、そのようなケースが多発し、そのたびに使用メモリが増えて行くことになります。
言ってみれば、回収はされてるんだけど間に合ってない、という感じですかね。
上のパッチでメモリが増えなくなるのは、ファイナライザつきオブジェクトからのリーチャビリティを明示的に切ることで、即時回収率が上がっているからだと思われます。
要らなくなったポインタを積極的にNULLにしてやるようにすれば防げそうです。
# 名前が…
上のコメントも私です。
#
正直ふじさわには半分ぐらいしか理解できないですが、それでも説明を読んでいると面白く感じます。
Trackback