Gauche:OOM

Gauche:OOM


要求

Gaucheは現状(0.8.13)、メモリを使い尽くした時は、Scm_Panic()を実行して、その時点で即座に終了します。

;; メモリが1G以下のマシンで
$ gosh
gosh> (make-vector 530000000)
GC Warning: Out of Memory!  Returning NIL!
out of memory (2120000008).  aborting...

これで困る事は普通は無いですが、特定の状況では、これでプロセス自体が終了してしまうのが嫌な場合があります。


仮パッチ

nekoie: 前述の問題に対する仮パッチを作ってみました。

パッチの概要

パッチは短いので、直接見てもいい気がします。

この挙動により、OOMが発生した場合は、以下のどれかの状況になります。

  1. 突然、超巨大な領域が要求された場合は、ただ単にエラー例外が投げられて失敗する(安全)
  2. 慢性的にメモリが不足してOOMが発生した場合
    1. OS自体のoom-killerによって、やっぱりプロセス自体が殺される
      • 自分の環境で「(let loop ((r '())) (loop (cons 1 r)))」とかのコードで試したら、そうなった(linux-2.6.11)
    2. 一時的に空きメモリがわずかに回復してから、エラー例外が投げられる。そして……
      1. エラー例外によって(ある程度の)大域脱出がなされる事で、GC可能な領域が増え、システム自体は維持される
      2. エラー例外によって(ある程度の)大域脱出がなされる途中で、guardやdynamic-windのafter thunkに引っかかって、そこでまたメモリが消費され、二度目のOOMが発生してScm_Panic()で終了

さっき例に出した、ブラウザ組み込みのJavaScript的な状況では、1番目の状況を回避できる事が重要です。

問題点

Scheme上での、OOMのセマンティクスは?

このパッチでは、OOMが起こったら「その場所からエラー例外を投げる」だけです。

  1. 単にエラー例外を投げるだけの場合、「単純なconsですらエラー例外が発生する可能性が常にある」という事で、厳密なコードを書く場合、ほぼあらゆる場面でエラー例外が発生する可能性を考えなくてはならなくなる
    • 現実的には、そこまで考える必要はまず無いとは思うけれど
  2. じゃあ、どうすればいいのかと言うと、「途中でOOMが発生したevalは信用ならないので、もう途中で中断する」という扱いにするのが妥当?
  3. ok。じゃあ、Scm_Eval直後で、脱出用継続を保持しといて、oom_handler()からそれを辿って、Scm_Evalまで戻ってから改めてエラー例外を投げる事にしようか。
  4. でも、OOMが発生する直前とかに、eval内からeval外の変数に継続をset!されてたら、せっかく抜けても、またevalが再実行されそうなんだけど?
  5. それと、継続を使って抜ける場合は、dynamic-windのafter thunkが実行される気がするけど、いいの?
    • 仮に無理矢理dynamic-windのafter thunkを実行せずに抜けたとして、Scheme的にはそれでいいの?
      • 例えば、さっきの、eval外の変数にset!された継続を辿って再突入した場合、after thunkは実行されてないのに再度before thunkは実行される事になるけど、これって、beforeとafterの対応が取れなくなるよね?
  6. そう考えると、dynamic-windのafter thunkは実行されるべきだよね。でも、OOM発生してるのに、そんな余裕あるの?下手したら二回目のOOMが発生して結局、Scm_Panic()されそうだけど。
  7. そもそも、OOMのScheme的セマンティクスって、どんな扱いになるべきなの?無限エクステントと相反してる気もするんだけど?

このパッチの結論

おまけ(このパッチの為に調査した結果など)

別の用途にも再利用できそうな気がするので、情報として残しておきます。 間違いがあったらごめんなさい。

SCM_MALLOC/SCM_MALLOC_ATOMICをカスタマイズする際の注意点

クリティカルセクションについて

具体的に、Gauche-0.8.13のソースを見て、必要だと判定された部分は、全部で以下の通り(※目でチェックしたので漏れのある可能性があります。また、バージョンが進むと増えたり減ったりするでしょう)

こうして見る限りでは、パフォーマンスに影響を与えそうな箇所は、Scm_MakeKeyword()ぐらいのように思えます。

議論

Shiro(2008/04/16 15:41:20 PDT): これは結構大きい問題なんで、ページを別にして 議論しませんか?

で、だんだん思い出して来たんですが、クリティカルセクションの中 (SCM_INTERNAL_MUTEX_LOCKとSCM_INTERNAL_MUTEX_UNLOCKに挟まれてる間)に Scm_Consを呼ぶケースってのはあったように思います。Scm_Consはエラーを 投げない (OOM時はそもそも帰ってこないので) という前提だっので。 そういう箇所をすべてSCM_UNWIND_PROTECTすると性能に大きな影響が 出そうに思えます。

漸進的にOOMになるケースがどうせ救えないのであれば、 巨大なメモリチャンクのアロケーションが要求される可能性のある 個別のケースに限り、特別な処理を入れるべきなのかもしれません。

nekoie(2008/04/17 04:41:49 PDT): 了解です。とりあえずページを作ってみました。

なるほどです……。 それだと確かに全コードをチェックして直していく必要がありますし、 性能面への影響も大きそうですね。 逆に、巨大なメモリ確保時のOOMの対応だけでいいのなら、 事前にOOM用領域を(再)確保したりする必要はないし、 性能的にも影響はほぼ無いですね。 「巨大なメモリ確保」かどうかをどうやって判定すべきかの問題はありますけど。

koguro(2008/04/18 09:13:20 PDT): 元々やりたかったことからすると、OOM対策を施した特殊なevalを作ればいいんじゃないかな、と思って上記のパッチの方向性とは若干異なりますが拡張モジュールを作ってみました(http://homepage.mac.com/naoki.koguro/prog/sandbox-0.0.tgz )。やっていることは以下の通りです。

適当に作ったものなので、スレッドセーフではありませんし、上で書かれているクリティカルセクション中にOutOfMemoryが発生した場合はデッドロックが起こる、といった問題を抱えていますが、こんなアプローチもあるんじゃないかなと思いました。あと、無名モジュールの領域が本当にGCされるタイミングがよく分からないので、nekoieさんのパッチのように予約領域を確保した方が安全かもしれません。

ちなみにこんな感じで使えます(Gauche本体へのパッチはいりません)。

gosh> (use sandbox)
#<undef>
gosh> (define sandbox (make-sandbox))
sandbox
gosh> (while #t (sandbox-eval `(define ,(gensym) (make-vector 1000000)) sandbox))
(.. たくさんGC Warningがでる ..)
GC Warning: Out of Memory!  Returning NIL!
*** ERROR: Out of memory
Stack Trace:
_______________________________________
  0  (sandbox-eval `(define ,(gensym) (make-vector 1000000)) sandbox)
        At line 3 of "(stdin)"
gosh> (gc-stat)
((:total-heap-size 3645329408) (:free-bytes 666996736) (:bytes-since-gc 56020976) (:total-bytes 2982020496))
gosh> (gc)
#<undef>
gosh> (gc-stat)
((:total-heap-size 3645329408) (:free-bytes 1019314176) (:bytes-since-gc 1152) (:total-bytes 2982023504))
gosh> (define a (make-vector 1000000))
a
gosh>

Tag: GC

More ...