非同期I/Oライブラリ
nmoshに非同期I/Oライブラリを搭載することにした。moshのportが深刻に遅い*1のと、nmoshでネットワークサーバを書く必要が生じてきたので。
他のSchemeに似たような機能をもったものは無いようなので(Gaucheのようにselectを持っていることはあるが。。)、APIはRubyのEventMachineを参考にした独自の物になる予定。
AMD E-350だと、128Bytesパケットでだいたい1MB/s出るので、8kps程度というところ。もちろんカリカリに最適化すればもっと高速になるが、言語機能をリッチに使ってわかりやすく書くのも重要なので。。
同期プリミティブにはI/O完了ポート(I/O completion port)を使っている。Linuxではepollを使う予定。
転送レートがぎざぎざになっているのはBoehmGCが走るため。ヒープ消費量が増え続けるのはおそらくFalse pointerのせいで、moshはクロージャを大量につくるとメモリリークを起こしやすい傾向がある。
ちなみにVisual Studioでコンパイルすると性能は4割程度低下する。これは、Visual StudioがUTF-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:文字列が一文字づつしか出力できない