formatterで高速化
【追記】内容が間違っていたので何回か書き直しました。
Common Lispの、formatter
マクロの使い方を紹介します。
このマクロは、format
の制御文字を受け取って関数を返却するものです。
とりあえず例をあげます。
下記のformat
文を考えます。
(format t "Hello~%")
実行すると次のような出力をします。
Hello
それに対してformatter
は、ただ関数を返却するだけです。
とりあえず実行してみます。
(formatter "Hello~%") -> #<FUNCTION>
関数が返却されました。
これを実行してみるとどうなるでしょう。
(funcall (formatter "Hello~%") *standard-output*) Hello
ちなみにfuncall
はnil
を返却します。
これは一体なにが楽しいのでしょうか?
formatter
はformat
関数を高速化するためのものです。
大きく、下記の2つのアプローチから高速化を実現します。
まず「構文解析を事前に行う」とはどういう事でしょうか。
構文解析とは、例えば"Hello~%"
という文字を見て、
①Hello
の出力と②terpri
の出力に分けることです。
format
の制御文字は文字列によりゴチャゴチャと記載されているため、
一文字ずつ調べて行き、~
文字が現れたら
さあどうしようかと考える処理が必要になります。
制御文字を一つずつ読んで解析するのはそれなりに時間がかかる作業です。
この解析を行うのがformatter
マクロの仕事になります。
もう一つの「コンパイルにより高速化を行う」というのはどういう事でしょうか。
それはformatter
を用いることでformat
の制御文字そのものを
機械語の変換してしまおうというものです。
npt
はインタープリタなのでその恩恵は得られませんが、
compile
を実装したLisp処理系だと、
format
制御文字をLisp式にさえ変換できれば
コンパイルで機械語に変換できるので、
かなり高速に実行できるようになります。
formatter
による高速化の作業は、実際にコードが実行する時点より
遥かに前で終わらせておくことができます。
例えば次のような文を考えます。
(dotimes (i 5) (format t "Hello: ~A~%" i))
難しいことは何もない文ですね。
実行結果を下記に示します。
Hello: 0 Hello: 1 Hello: 2 Hello: 3 Hello: 4
この文の問題点は、"Hello: ~A~%"
という文字を
5回も構文解析している点にあります。
「先頭にHello:
があってprinc
の次にterpri
がある」という分析は、
何も律義に5回実行する必要なんてありません。
それに今回は5回で済んでいますが
何万回も出力するということは珍しくないと思います。
そういうときにformatter
マクロの出番です。
(setq *format* (formatter "Hello: ~A~%")) (dotimes (i 5) (funcall *format* *standard-output* i))
いいですね。
構文解析が1回で終わっています。
【変更】
さらに次のように書き直すことができます。
(dotimes (i 5) (format t #.(formatter "Hello: ~A~%") i))
formatter
の結果を#.
によりread
時に実行するように変更しています。
すると全体では下記のような文になります。
(dotimes (i 5) (format t #<FUNCTION> i))
なぜformat
の引数にformatter
を指定しているのか疑問に思うかもしれません。
実はformat
関数は、制御文字の引数に関数そのものを受け取ることができます。
ちょうどformatter
の返却値を受け取るように考慮されているのです。
実際、やっていることは次の命令と変わりません。
(dotimes (i 5) (funcall #<FUNCTION> *standard-output* i))
以上でformat
関数をより高速に実行する方法を覚えました。
では実際にやってみましょうということで、 速度を測定してみたりしても 全然変わらないじゃないかと思う人もいるかもしれません。
実際、全然変わっていない可能性が高いです。
なぜなら、Lisp処理系がeval
時に勝手にformatter
を実行しているからです。
つまり、
(format t "Hello~%")
と記載していたにも関わらず、 処理系はお節介にも
(format t (formatter "Hello~%"))
と変換して実行しているのです。
本当に?
調べる方法があります。
次の例文を考えましょう。
(format t "ERROR ~! ERROR")
見てわかるように、~!
なんて命令は無いので不正な文です。
実行してみましょう。
(format t "ERROR ~! ERROR") →エラー、~!なんて知らない
そりゃそうです。
しかしlambda
で囲んだらどうなるでしょうか。
(lambda () (format t "ERROR ~! ERROR"))
普通に考えるならば、実行結果は何も問題が無いはずです。
では実際はどうなるでしょうか?
各処理系で実行した結果を示します。
sbcl
WARNING
,~!
がおかしいFUNCTION
返却
clisp
FUNCTION
返却
ccl
WARNING
,~!
がおかしいFUNCTION
返却
clisp
は素直にlambda
で囲んだ分を返却しています。
しかしsbcl
とccl
はWARNING
が出力されているため、
format
の制御文字を構文解析しようと試みたのが分かります。
この事前処理を行おうとしたのはeval
の最適化です。
もしformat
文の2番目の引数が文字列だったら、
先行してformatter
を実行しておこうした結果です。
これはありがたい。
何も考えなくてもいいんですから。
しかし最適化が見てくれているとしても、
例えば実験データなんかを大量に出力するということが分かり切っている場合は、
format
文の制御文字の代わりにformatter
を使っておくのはいい考えだと思います。
ちなみにsbcl
だと、optimize
のspeed
を0
にすることで
この機能を解除することができました。
(locally (declare (optimize (speed 0))) (lambda () (format t "ERROR ~! ERROR"))) -> WARNINGは出ない -> FUNCTION返却
書き直しについて
最初は、#.
をつけずに下記のように実行することを考えていました。
(dotimes (i 5) (format t (formatter "Hello: ~A~%") i))
しかしどの処理系を見てもformatter
はlambda
式を返却するため、
これだと構文解析は1回で終わるのですが、実行するたびに関数が生成されてしまいます。
つまり上記の例では、lambda
により5個の無名関数が生成されてしまいます。
formatter
が無いときに比べれば早くなるので悪くはないのですが、
メモリを無駄に使うのも良くはありません。
確実に#.
を付けた方がいいでしょう。
しかし#.
による仕組みは、見た目は綺麗ですが結構変なことをしていますので、
setq
やlet
なんかでformatter
の関数を
変数に束縛するのが一番素直で確実なのかもしれません。
つまりはこんな感じ。
(setq *format* (formatter "Hello: ~A~%")) (dotimes (i 5) (format t *format* i))