Common Lispでyesコマンドを実装した
この記事 Haskell で yes コマンドを実装した に触発されて、yesコマンドをCommon Lispでモダンに書いてみました。
速度があまり速くなく、識者のご指摘をいただきたいところです。
2017/12/01追記 ご指摘いただけました! 後ろの方に追記しました。
コード
package-inferred-systemで定義されていてroswellスクリプトをもつ、こんな感じの構成です。トリビアル。
cl-yes$ tree . ├── README.md ├── cl-yes.asd ├── main.lisp └── roswell ├── yes └── yes.ros 1 directory, 5 files
速度測定
では、速度を測定します。
GNU Coreutilsのyes
リファレンスとして、ふつうのyes
の速度を測ります。
$ yes | pv -r > /dev/null [9.28GiB/s]
あらまー。10秒走らせたら100GBですって。はやいですねー。
yes.ros
Common Lispの処理系はこんな感じでございます。
$ ros config externals.clasp.version=5.0 setup.time=3718530120 sbcl-bin.version=1.4.1 default.lisp=sbcl-bin
文字列を出力しつづけるyes関数はこんな感じでございます。シンプル。
(defun yes (out &optional (s "y")) (loop (write-line s out)))
とりあえず実行してみると、
$ ./roswell/yes.ros | pv -r > /dev/null [1.86MiB/s]
ええ、遅い…。ビルドしてみるとどうなるでしょうか。
$ ./yes | pv -r > /dev/null [3.04MiB/s]
出だしの速度が改善されただけでした(ビルド前も5秒待つと、これくらいの数値)。なにが原因なのでしょう。試しにprinc
関数に書き換えてみます。
(defun yes (out &optional (s "y")) (loop (princ s out) (terpri out)))
$ ./yes | pv -r > /dev/null [4.62MiB/s]
ちょっとだけ速くなりましたが、なんでprinc
のほうが速いんだ。format
でも同じくらい。
(defun yes (out &optional (s "y")) (loop (format t "~a~%" s)))
$ ./yes | pv -r > /dev/null [3.59MiB/s]
ちなみに最適化宣言をしても、とくに結果は変わりませんでした。
(defun yes (out &optional (s "y")) (declare (optimize (speed 3) (debug 0) (safety 0))) (loop (format t "~a~%" s)))
$ ./yes | pv -r > /dev/null [3.04MiB/s]
というか、むしろちょっぴり遅くなった…?
そもそもGNU coreutilsのyesが謎の速さであって、そっちがおかしいのだろうか…。
ううむ、識者のご指摘がほしい…。
速い出力への道 (追記)
この記事を公開したところ、佐野さんから「バッファすると速いよ」とご指摘いただきました。そして併わせて、CFFIを通じてCの文字列でバッファする実装のコードが。こちらを手元の環境で実行すると、本物のyes並の速度が得られました…!
$ ./yes | pv -r > /dev/null [9.33GiB/s]
同じ内容を吐くだけなら一度だけバッファに溜めて、あとはそれをまとめて出力すればいいのです。目的に合わせて柔軟に思考しましょう。GNU Coreutilsの実装もまさにそのようになっています。
では、これをCommon Lispだけでやるとどうなるのか。ちょっと気になりますね。なのでやってみました。
(defun make-buffer (s &optional (times 10000)) (with-output-to-string (out) (dotimes (n times) (write-line s out)))) (defun yes (out &optional (s "y")) (let ((buf (make-buffer s))) (loop (princ buf out))))
$ ./yes.ros | pv -r > /dev/null [ 358MiB/s]
速度は、愚直な実装よりはずいぶん速くなりましたが、バッファのサイズに関わらずこの数字で頭打ちになります。GNU Coreutils yesの4%くらい…。Common Lispのストリームを挟むと遅いんでしょうかね。
まとめ (追加)
Common Lispにおいて猛烈な出力速度が求められる場合は、CFFIを使ってCの配列に出力内容を自前でバッファするのがベストプラクティスであるようです。