Gauche:BufferedIO
Gauche-0.5.4に入る(予定)バッファードI/Oルーチンについてのメモ。
- 入った。
0.5.3までの問題
FILE* 構造体
0.5.3までは、通常のファイルI/OはFILE* を通して行っていた。 これは簡単なんだが、いろいろ不便なこともある。
- ポータブルにchar-ready?を実装する方法がない: ファイルディスクリプタにデータがavailableかどうかはselect(2)などで 確かめられるが、バッファ内にまだデータがあるかどうかはFILE構造体の 中身を見ないとわからん。大抵のシステムではFILEの中身は似たような ものだが、非公開の構造体の中身に依存すると思わぬところでつまづくことがある。
- データ長を正確に指定した読み込みがやりにくい: バッファの中身が足りなくなると、システムはそれを埋めるためにread(2)を 呼ぶが、時にはそのread(2)で読み込むデータサイズを指定したいことがある。
- マルチバイト文字列絡みの最適化がやりにくい: 例えばread-lineは、バッファ内で'\n'をスキャンすれば読み込み時に mb->char変換をいちいち行わなくても良い、など。(getcを使って 1バイトづつ読んでScmDStringに追加していっても良いが、 行がバッファ内に収まっている場合、バッファを直接スキャンできれば ScmDString自体を使う必要がなくなる)。
それに、charconv絡みで既に自前でバッファリングを行うポートも 実装されているんだし、それなら最初からバッファードI/Oを自分で やることにしてもコードサイズは増えないだろう。
マクロの濫用
0.5.3までのSCM_GETC等は既に巨大なマクロになっていて、ちょっと気になっていた。 果してこれらをマクロ展開することは性能に寄与してるんだろうか。 関数呼び出しにしても、そいつがIキャッシュに乗っかってしまえば さしてペナルティは無く、むしろ全体のコードサイズが小さくなるぶん 有利なんじゃないか。
というわけでFILE*を使わずに自前でのバッファードI/Oに切り替えてみる。 port.cを大幅に書き換え。基本的なところは一日で動くようになったが、 いくつか落し穴が。
exit時のバッファのフラッシュ
exit(3)はシステムのFILE* をフラッシュしてくれるが、ユーザが実装した バッファは知ったこっちゃないので、Scm_Exitの中からフラッシュするようにする。
ところが、だ。フラッシュするためには現在オープンされている出力ポートを 把握しておかなければならない。ポートがオープンされた時点で内部のリストに つないで、クローズされた時点ではずすようにすると、作りっぱなしで参照 されなくなったポートがGCされなくなってしまう。
いくつかfinalizationを使ったトリックを考えてみたが、 結局素直にweak pointerを実装するのが一番よさそうだ。 Boehm GCにはweak pointerを実装するAPIがある。
Schemeレベルにweak pointerを見せる方法としては、
- weak vector (Tとか)
- weak hash table (MzSchemeとか)
- weak box (MzScheme)
- weak pair (ChezSchemeとか)
などがあるが、Boehm GC的にはweak vectorが一番素直そうなのでそうする。 (weak pointerはATOMICでアロケートしなければならないので、 weak boxやweak pairだと1オブジェクトあたり2回アロケーションが 必要。weak vectorならベクタサイズ分のオブジェクト全体に対して2回で良い)。 weak hash tableは便利そうだが、必要ならweak vectorの上に作れるだろう。
というわけでport.c内部でオープンされた出力ポートへのweak pointerを 管理することでExit時のフラッシュの問題は解決。
バッファリングモード
出力に関してはとりあえずstdioと同じように「出来る限りバッファ」 「ラインバッファ」「バッファ無し」を用意。 但し、「出力先と同じターミナルから入力が要求された時にはフラッシュ」 はやらない。明示的にflushすればいいんじゃなかろうか。
入力では「ラインバッファ」の意味はあまりない。その時点でavailableな データをすべてバッファに取り込むか、要求されたサイズだけの取り込むか という違いがあるだけだ。但し、要求されるデータのサイズによっては サブシステムが2回以上read(2)を呼ばねばならない場合がある (バッファの 容量より大きなデータが要求された場合など)。その場合、 たとえばcallerがあらかじめselect(2)でデータがあることをチェックしていた としても、2回目のread(2)でブロックする可能性がある。アプリケーションによっては それは避けたいだろう。ということで、以下の3モードを用意する。
- バッファリングなし : 常に要求されたぶんだけread(2) を呼び、 要求サイズより少ないデータが帰って来た場合でもそれを容認。
- フルバッファリング : 常にバッファを満たすぶんだけread(2)を呼び、 要求サイズより少ないデータが帰って来た場合は要求サイズを満たすまで 繰り返しread(2)を呼ぶ。
- modestバッファリング:常にバッファを満たすぶんだけread(2)を呼ぶが、 要求サイズより少ないデータが帰って来た場合はそれ以上read(2)を呼ばずに 返す。
他に必要なモードってあるかなあ。
例外コンディションの管理
feof(3)とかferror(3)とかに相当するAPI。まだつけてない。どうしようかなあ。
結果
ナイーブな実装でも、0.5.3より良い性能が出てる。 2002/04/24 21:41:08 PDT時点で、単純なキャラクタI/Oの繰り返しで10%くらいの改善。 read-line等はこれからいじる。
コードサイズが10%くらい小さくなった。やっぱりGETCマクロはでかすぎたようだ。