週刊mosh - リアルタイムアプリケーション / リストのpack

moshは大きな進捗はなし。そういやlang/mosh( http://www.freebsd.org/cgi/query-pr.cgi?pr=158968 )のリプライを書かなきゃ。。FreeBSDのGNATSはメールを寄越さないのでpollingしないといけないのが辛い。

リアルタイムアプリケーション

今のところいくつかのゲームやインスタレーションを(n)moshでやっていて、

  • ヒープオブジェクトを作らない
  • 確実に待ちが入る所で明示的にGCする

が勝ちパターンになっている。明示的なGCはnmoshでないと出来ないが、ヒープオブジェクトを作らないのはpsyntax-moshを含め色々なScheme処理系で有効かつ基本的な対策だと思う。
問題は、この手の勝ちパターンはあまり処理系自体のユーザには見えない点で、処理系を書いてる本人でないとできない最適化が存在していることになる。(たとえば、何がヒープオブジェクトになるのかを知らないとヒープオブジェクトを避けるのは難しい。)
もっとも、大抵の処理系はユーザが"多少は"賢いことを期待しているように思える。"どう書いても速い"というのは非常に実現が難しい。

ゲームアプリケーションとGC

ゲームはリアルタイムアプリケーションとしては微妙に特殊な部類に入っていて、"一画面分の描画"を60Hz*1で行わないといけない。
この60HzはVSYNC周波数で、画面更新の数に対応している。逆に言えば、一般に一秒間に60枚以上の画面を生成しても表示できないし、単純に無駄になる。
60Hz = 16.67ミリ秒なので:

  1. ゲームパッド/各種センサからの入力を読み取る
  2. ステートマシン・物理シミュレータを駆動し、ゲーム状態を1ステップ進める (→キーが押されていたらキャラクタの位置を進める等)
  3. ゲーム状態を描画リストに変換する
  4. 描画リストをkickし、GPUにゲーム画面を描画させる
  5. 次のVSYNCまで待つ

の各ステップを16.67ミリ秒で行わないといけない。
moshの典型的なGC時間は30ms〜1000msあるので、次のVSYNCまでの待ち時間にGCを発動することになる。moshのBoehmGCはStop the world GCなので、GC期間中には何も出来ない。何も出来ない期間を30msも取られてしまうと、確実に1フレームの遅延が発生してしまう。
BoehmGCはIncremental GCとして構成することもできるが、これはヒープの一部を書き込み禁止にすることによって実現される(Incremental GCを正常に動作させるためには、新しい参照の発生をトラップするためにヒープへの書き込みを全部トラップする必要がある)ため、ヒープオブジェクトを外部APIや外部デバイスに渡すときに問題が起こる。
(典型的には、外部APIや外部デバイスが、ヒープオブジェクトへの参照を発生させることは無いので、これらに渡されるヒープオブジェクトを特別扱いするという手は一応ある。)
というわけで、今回はGC時間自体を短くするために"止まっても良いタイミング"で明示的にGCを呼んでいる。ゲームの場合はVSYNCまでの待ち時間ということになる。
一般的なアプリケーションにはなかなか"止まっても良いタイミング"は訪れないので、結局のところGCの側でこのようなシチュエーションを良くサポートしてやる必要は有る。

リストのpack

nmoshは構造体を定義してpack/unpackすることでbytevectorで表現されたパケットをオブジェクトに変換したりその逆ができる。
これを同様に適用して、Schemeオブジェクトのリストで表現されたものをpackすることでGPU向けの描画リストを作れると嬉しいが、これが地味に難しい。
GPU描画リストの生成はOpenGLとかDirectXのようなAPIによって抽象化されている。よって、リストをpackした結果は高々API呼び出しのリストにしかならない。(もっとも、現状のnmoshのcairoバインディングFFI呼び出しのオーバヘッドを避けるためこれをやっている。)
究極的には、描画のためのシンボルやパラメタをconsした瞬間に(非同期に)APIを呼び出してハードウェアのためのリストを構築すべきといえる。ただ、これをやるためには生成されるリストがScheme側で消費されないことを保証する必要がある。

*1:TVゲームだとPAL地域対応も必要なので50Hzというケースもある。