「Ansi Common Lisp」13章読了
スピードと題して。Lispは、
- 高速のプログラムを書くための言語であり、
- 高速にプログラムを書くための言語である。
だそうです。
それって二律背反のような気がするのですが、どうなんでしょう。
最近の言語は前者を犠牲にして、後者を取ってますよね。
あとこの手の話題で取り上げられるのは、言語としてのメンテナビリティでしょうね。
その点Common Lispは(慣れの問題もあるのでしょうが)あまり良くない気がします。
ボトルネックルール
2:8ルール(ここでは3%ルールで書かれてますが)とか、最適化はプログラムが動いてからにしろとか、一般的な話。
コンパイル
declareで局所的に、declaimで全体的にコンパイルオプションを指定できるというもの。
コンパイルオプション指定なし
(let ((a 0)) (dotimes (x 1000 a) (dotimes (y 1000 a) (setf a (+ a 1)))))
declareで局所的にコンパイルオプションを指定
(let ((a 0)) (dotimes (x 1000 a) (dotimes (y 1000 a) (declare (optimize (speed 3) (safety 0))) (setf a (+ a 1)))))
declaimで全体的にコンパイルオプションを指定
(declaim (optimize (speed 3) (safety 0))) (let ((a 0)) (dotimes (x 1000 a) (dotimes (y 1000 a) (setf a (+ a 1)))))
実行した感じ、あまり速度は変わりませんでした。
まあ、こんなただ二重ループしてるだけのプログラムじゃあ、最適化も何もないのでしょうけど。
型宣言
パフォーマンス向上目的で型宣言することもできますよ。ということらしい。
ほんとLispって柔軟ですよね。
(declaim (fixnum a) (fixnum x) (fixnum y)) (let ((a 0)) (dotimes (x 1000 a) (dotimes (y 1000 a) (setf a (+ a 1)))))
上で書いた合計プログラムの型宣言版。
やっぱりあんまり速度は変わりませんでした。。
ごみ回避
ガーベジコレクタが遅かった時代は、コンシングを減らすことで速度向上が期待できたが、最近ではそうでもないとのこと。
ここでは、破壊的関数を使うことでコンシングを減らす(removeに対してdeleteとか)ことと、オブジェクトをスタックに割り付けるように指定する方法が書いてある。
関数の引数(レストパラメータ)をスタックに割り付ける例が載ってるんですが、
関数の引数って、元々スタックに割り当てられるものじゃないの?
高速なオペレータ
シークエンス一般に使えるetlよりも、ベクタであればsvref、リストであればnth、ストリングであればcharを使った方が速いよみたいな話。
4章で出てきた「特別なデータ構造」は「高速なアクセスとスペースの節約」が目的でしたが、それの再出っぽい。
2段階の開発
最初はLispで作って、高速化したいところをCで置き換えるみたいな話。
ここに書かれているメタファが面白いので転記してみる。
たとえば、ブロンズ像を作る標準的な手続きでは、最初、粘土を使う。粘土の像を初めに作って、その後、それを使って型を作り、その型を使ってブロンズ像が作られる。粘土は最後の像には残っていないが、その効果はブロンズ像に見ることができる。ブロンズのかたまりとのみでスタートして同じものを作ろうとしたらどうなるか、その困難のほどを想像してみるとよい。同じ理由で、プログラムをまずLispで書き、それからCで書き直した方が、最初からCで書こうとするよりもよいことがある。
「プログラムをまずLispで書き」の部分は、別の言語に置き換えても成り立ちそうですね。