非同期I/Oライブラリ

nmoshに非同期I/Oライブラリを搭載することにした。moshのportが深刻に遅い*1のと、nmoshでネットワークサーバを書く必要が生じてきたので。
他のSchemeに似たような機能をもったものは無いようなので(Gaucheのようにselectを持っていることはあるが。。)、APIRubyのEventMachineを参考にした独自の物になる予定。

AMD E-350だと、128Bytesパケットでだいたい1MB/s出るので、8kps程度というところ。もちろんカリカリに最適化すればもっと高速になるが、言語機能をリッチに使ってわかりやすく書くのも重要なので。。
同期プリミティブにはI/O完了ポート(I/O completion port)を使っている。Linuxではepollを使う予定。
転送レートがぎざぎざになっているのはBoehmGCが走るため。ヒープ消費量が増え続けるのはおそらくFalse pointerのせいで、moshクロージャを大量につくるとメモリリークを起こしやすい傾向がある。
ちなみにVisual Studioコンパイルすると性能は4割程度低下する。これは、Visual StudioUTF-32文字列リテラルを扱えないので、関数の呼び出しのたびにハッシュテーブルを参照するため。 リンク時最適化をすればVisual Studioの方が倍くらい早くなる。。gcc 4.6.0を試すまで勝負はお預けのようだ。
moshのこの辺の問題には解決策が無いように思えるので、"R6RS準拠な制御/デバッグ/REPL用VM"(= 現在のVM)と"R6RS-αでコンパクトなリアルタイムVM"の2VM体制にすることを考えている。
VMを2つ持たせるとオブジェクトのやりとりが問題になる。これはmosh自体がbootstrapのためにやったように、VM自体をSchemeで書くことによって解決できるような気がしている。通常のbootstrapと違うのは、Schemeでヒープフォーマットも実装する必要がある点だろう。。

プログラム

一番下のレイヤ、つまり、win32 APIを叩いている部分では、いわゆる継続サーバのような書き方になっている。reactorパタン。
Win32のI/O完了ポートは単純なqueueで、I/OのリクエストとOVERLAPPED構造体を関連付けて、リクエストの完了時にOVERLAPPED構造体のポインタを受け取ることができる。
今回はこのOVERLAPPED構造体を拡張して、OVERLAPPED構造体の他にSchemeオブジェクトを追加できるようにした。つまり、OVERLAPPED構造体に継続をくっつけてenqueueし(win32_handle_*_async手続きとwin32_socket_connect手続き)、queueから取り出したらその継続を評価するようになっている。

;; overlapped-delay 構文。継続のポインタをOVERLAPPED構造体にくっつける。
(define-syntax overlapped-delay
  (syntax-rules ()
    ((_ form ...)
     (let ((proc (lambda () form ...))
           (ovl (win32_overlapped_alloc)))
       (win32_overlapped_setmydata ovl
                                   (object->pointer proc))
       ovl))))
 
;; overlapped-force 手続き。OVERLAPPED構造体にくっついた継続を取り出し、評価する。
(define (overlapped-force ovl)
  ((pointer->object (win32_overlapped_getmydata ovl))))

 
;; echoサーバと通信するサンプルクライアント。
;; 状態(継続)はconnect-k、read-k、write-kの3つで、connect-k→read-k→write-k→read-k→write-k→...
;; のように遷移する。
(define (client)
  (define buf (make-bytevector 128))
  (define recvbuf (make-bytevector 128))
  (receive (s c _) (win32_socket_create 4 1)
    (define write-k
      (overlapped-delay
        (win32_handle_read_async s 0 128 recvbuf read-k)))
    (define read-k
      (overlapped-delay
        (win32_handle_write_async s 0 128 buf write-k)))
    (define connect-k
      (overlapped-delay
        (win32_handle_write_async s 0 128 buf read-k)))
    (win32_iocp_assoc queue s pointer-null)
    (win32_socket_connect c s addrc lenc connect-k)))
 
;; メインループ。win32_iocp_popをつかって、完了したリクエストをpopし、forceする。
(define (loop)
  (receive (res bytes key ovl) (win32_iocp_pop queue 1000)
    (cond
      ((= res 1)
       (overlapped-force ovl))
      (else (display "fail...")(newline)))
    (loop)))

scheme的に馴染みの深いdelay構文とforce手続きの組み合わせで書けるようにしてみた。

*1:文字列が一文字づつしか出力できない