読者です 読者をやめる 読者になる 読者になる

read-char高速版?

sbcl

最近はパーサを書くことが割合多く、そして(僕が書くような)パーサでは、read-char関数の呼び出し部分が、全体の中で一番大きなボトルネックになっていることが少なくない。

そういった場合、これまではread-char関数の呼び出し回数を減らす(アルゴリズムやロジックを改良する)という方法で高速化を図っていたのだが、最近(JSONのパーサを書いている時に)sbclに依存して良ければ、read-char関数の呼び出し自体も高速化できることが分かった。

結論から云えば、sb-impl::ansi-stream-read-char関数を使えば良い。

こっちの関数なら、read-charに比べて、60%程度、実行時間が短縮できる。

見ての通り、common lispの標準関数でなく、しかもsb-implの内部シンボル(exportされていないシンボル)なので、積極的に使うべきではないだろう。
ただ、一応こういった関数があることを知っておいて、sbclに依存しても良いのでどうしてもread-char関数の処理速度を早くしたい、という時には使うのもありなのではないかと思う.

ansi-stream-read-char関数の制限

ansi-stream-read-char関数は、read-char関数に比べて高速な分、機能的な制限がある。 ※ 渡す引数は基本的には変わらない

まず、read-char関数の実装を見てみる。

;; sbcl-1.0.28/src/code/stream.lisp
(defun read-char (&optional (stream *standard-input*)
                            (eof-error-p t)
                            eof-value
                            recursive-p)
  (let ((stream (in-synonym-of stream))) ;引用注: in-synonym-ofは、tやnilを、適切なstreamに変換するマクロ
    (if (ansi-stream-p stream)
        (ansi-stream-read-char stream eof-error-p eof-value recursive-p)
        ;; must be Gray streams FUNDAMENTAL-STREAM
        (let ((char (stream-read-char stream)))
          (if (eq char :eof)
              (eof-or-lose stream eof-error-p eof-value)
              char)))))

これによれば、read-char関数は、呼び出された時に、いろいろな前処理を行い、その後ansi-stream-read-char関数(or 条件によってはstream-read-char関数)を呼び出して、文字読み込みの処理を行わせている(多分)。

つまり、その前処理がない分だけ、ansi-stream-read-char関数の方が速くなる分けだが、次のような制限も出てしまう。

  • optional引数を使えない
  • 第一引数にtやnilを渡せない
  • gray streamsを扱えない

どれもそれほど強い制限ではないが、一応注意が必要だ。
それはそうと、read-charを呼び出す度に、毎回stream引数の変換やansi-streamかどうかの判定を行うのはどうかと思う。lispでは仕方がないとも思うが、C++のようなオブジェクト指向言語なら、この二つの処理はコンストラクタ関数内で済ませておくことが出きるので、文字読み込みの際の余計なコストをなくせるのに、と思ってしまう