nmoshがメモリを食う理由

nmoshには3つの大きな問題があって:

  • メモリを食う(ことがある)
  • expandが遅い
  • explicit-phasing

このうち、(特にWin32環境で)メモリを食うのがそろそろ実際に問題になってきたので、GC_DEBUGやデバッガを駆使して調べたところ、理由が2つわかった。

スタックやベクタをIGNORE OFF PAGEでアロケートしていない

Boehm GCで普通にGC_MALLOCしたオブジェクトは、"そのオブジェクトの一部分でも"指されていれば回収されない。しかし、moshVMスタックやvectorは常に先頭からのオフセットでアクセスするので普通のGC_MALLOCを使う必要は無い。これらをGC_MALLOC_IGNORE_OFF_PAGEで確保するようにしたら、テストコードでは1300MB→1000MBにメモリ消費量が減った。
この減った分はおそらくfalse pointerによって回収されずに残っていた拡張前のVMスタックで、少なくともVMスタックに関してはIGNORE_OFF_PAGEで確保するように変更するのは価値があるように思える。
基本的には、moshが確保する全てのvectorGC_MALLOC_IGNORE_OFF_PAGEで確保できるように思えるが、そのように変更するとpsyntax-moshが複雑なプログラムで落ちるようになるので、どこかにオフセットでアクセスしていないベクタが存在する。
moshVMスタックを拡張する際、新たな配列を確保して内容をコピーする。このため、過去のスタックがfalse pointerで回収されないという悲劇が起こる(通常のプログラムでは確率はかなり低い)とスタックに積まれていたオブジェクトすべてが回収不能になってしまう。

call-with-valuesが末尾再帰でない

nmoshはコードの長さに応じてメモリを食うが、これはmoshのcall-with-valuesが末尾再帰的でないことに起因する。

(define (loop x)
  (receive (prop0 prop1) (get-properties x)
    (do-something x)
    (loop (cdr x))))

のようなreceiveの中から再帰するループはよく書きがちだが、moshでこれをやるといつかstack overflowすることになる。そしてnmoshのexpanderはループを全てこの形で書いている*1
通常のプログラムでこの挙動を避けるのは非常に簡単で、単にmoshの多値を使うのを止め、自前で多値を実装すれば良い(例えばchibi schemeは多値をエミュレーションする形で実装している)。ただ、expanderは自分自身でbootstrapしなければならないのでcall-with-valuesのようなプリミティブを置き換えることはできない。

*1:fluid-letがcall-with-valuesで全体を囲んでいるため。。