Posted 2015-08-02 07:48:07 GMT

Common Lispの FORMAT は多機能なので、

format T "~{~V@{~A~:*~}:~:*~A~*~%~}" ' 13 = 5 \# 22 % 18 + 40 % 2 *

こんなものを書いて悦に入ってしまうことも多いのですが、ふと、これは FORMAT の仕様上遅いなと思ったので、実際どうなのか計時してみることにしました。

put-graph-format は凝った FORMAT を使用、 put-graph は、 FORMAT は排除、 put-graph-mix は、まあ適材適所という感じです。

( dat out ) "~{~V@{~A~:*~}:~:*~A~*~%~}" dat format outdat defun put-graph-format (defun put-graph (dat out) (do ((x dat (cddr x))) ((endp x)) (let ((len (elt x 0)) (chr (character (elt x 1)))) (write-string (make-string len :initial-element chr) out) (write-char #\: out) (write-line (write-to-string len) out)))) (defun put-graph-mix (dat out) (do ((x dat (cddr x))) ((endp x)) (let ((len (elt x 0)) (chr (character (elt x 1)))) (write-string (make-string len :initial-element chr) out) (format out ":~D~%" len))))

計時

assert let 13 = 5 \# 22 % 18 + 40 % 2 * dat ' and string= with-output-to-string ( out ) ( put-graph dat out ) with-output-to-string ( out ) ( put-graph-format dat out ) string= with-output-to-string ( out ) ( put-graph dat out ) with-output-to-string ( out ) ( put-graph-mix dat out ) defvar *null-stream* make-two-way-stream make-concatenated-stream make-broadcast-stream (progn (let ((*print-pretty* nil) (dat '(13 = 5 \# 22 % 18 + 40 % 2 *)) (out *null-stream*)) (time (dotimes (i 100000) (put-graph dat out)))) (let ((*print-pretty* nil) (dat '(13 = 5 \# 22 % 18 + 40 % 2 *)) (out *null-stream*)) (time (dotimes (i 100000) (put-graph-format dat out)))) (let ((*print-pretty* nil) (dat '(13 = 5 \# 22 % 18 + 40 % 2 *)) (out *null-stream*)) (time (dotimes (i 100000) (put-graph-mix dat out))))

Timing the evaluation of (dotimes (i 100000) (put-graph dat out)) User time = 0.756 System time = 0.000 Elapsed time = 0.758 Allocation = 218460776 bytes 0 Page faults Calls to %EVAL 1800036 Timing the evaluation of (dotimes (i 100000) (put-graph-format dat out)) User time = 5.820 System time = 0.000 Elapsed time = 5.807 Allocation = 228846464 bytes 0 Page faults Calls to %EVAL 1800036 Timing the evaluation of (dotimes (i 100000) (put-graph-mix dat out)) User time = 0.536 System time = 0.000 Elapsed time = 0.535 Allocation = 208840096 bytes 0 Page faults Calls to %EVAL 1800036

計時結果

上記は、LispWorks 7の結果ですが、SBCLでもAllegro CLでも put-graph-format は大体7〜10倍位は遅いみたいです。

なぜ遅いのか

なぜ遅いのかですが、今回の場合、引数の処理の仕方に原因があります。

制御構造を整形して書くとこんな感じですが、

format T "~ ~{~ ~V@{~ ~A~:*~ ~}~ :~:*~A~*~%~ ~}~ " ' 13 = 5 \# 22 % 18 + 40 % 2 *

これは、大体こんな感じのことをやっているのと同じです。

format T lambda ( srm arg ) prog ( ( pos 0 ) v a ) L cond null nth pos arg return setq v nth pos arg incf pos let ( ( lppos pos ) ) dotimes ( i v ) setq a nth lppos arg incf lppos setq lppos pos princ a srm princ ":" srm srm decf pos princ v srm incf pos incf pos terpri srm go L 13 = 5 \# 22 % 18 + 40 % 2 *

引数リストを行きつ戻りつ、という感じですが、これはリストの構造上遅くなりそうというのは想像が付くと思います。

一応この方式も計時してみましたが、素の FORMAT のものより少し速い位のもののようです。

( dat out ) lambda ( srm arg ) prog ( ( pos 0 ) v a ) L cond null nth pos arg return setq v nth pos arg incf pos let ( ( lppos pos ) ) dotimes ( i v ) setq a nth lppos arg incf lppos setq lppos pos princ a srm princ ":" srm srm decf pos princ v srm incf pos incf pos terpri srm go L format outdat defun put-graph-foo (assert (let ((dat '(13 = 5 \# 22 % 18 + 40 % 2 *))) (and (string= (with-output-to-string (out) (put-graph dat out)) (with-output-to-string (out) (put-graph-foo dat out)))))) (let ((*print-pretty* nil) (dat '(13 = 5 \# 22 % 18 + 40 % 2 *)) (out *null-stream*)) (time (dotimes (i 100000) (put-graph-foo dat out)))) Timing the evaluation of (dotimes (i 100000) (put-graph-foo dat out)) User time = 2.524 System time = 0.000 Elapsed time = 2.519 Allocation = 163242472 bytes 0 Page faults Calls to %EVAL 1800036

まとめ

今回の話は、正確には、 FORMAT で凝ったこと全般についてではなく、 FORMAT の ~* (goto)の多用は理屈上遅くなるという話でした。

FORMAT を使った方が速くなる箇所もありますが( put-graph-mix の例)、色々凝った結果遅くなってしまう事例は、探せば他にもある気がします。

どうも凝ったことをしたくなりがちな FORMAT ですが、程々にしておくのが吉ではないかと思うのでした。

■

