2011-01-26

誰でもわかるわけないコンパイラマクロ

Common Lispのソースコードを読むとたまに出てくる、define-compiler-macro。これは何ぞや、と思ったことのある人も多いのではないだろうか。今回のテーマはコンパイラマクロ。

define-compiler-macroとは、その名の通り、コンパイラマクロを定義するマクロのこと。では、コンパイラマクロとは何なのか?

端的に言えば、マクロの一種だ。普通のdefmacroで定義するマクロと何が違うかというと、

  1. 他の関数やマクロと名前が重複しても良い
  2. コンパイラマクロを展開する前の式が&whole引数に束縛される
  3. funcallによる呼び出しも展開される
  4. 展開しなくてもいい

これを見ても、何に使うのかちょっと予想できないかもしれない。他と名前が重複したら、展開するときに困るだろうし、funcall? マクロなのに? それに、展開しないようにすることができるって、展開しないマクロって何なんだ?

一見不可解なこれらの特徴も、特定の場面ではとても便利に使える。最適化だ。

具体的な例を見た方が分かりやすい。HyperSpecのdefine-compiler-macroのExamplesから引用した。

(defun square (x) (expt x 2))
;; => SQUARE
(define-compiler-macro square (&whole form arg)
(if (atom arg)
`(expt ,arg 2)
(case (car arg)
(square (if (= (length arg) 2)
`(expt ,(nth 1 arg) 4)
form))
(expt (if (= (length arg) 3)
(if (numberp (nth 2 arg))
`(expt ,(nth 1 arg) ,(* 2 (nth 2 arg)))
`(expt ,(nth 1 arg) (* 2 ,(nth 2 arg))))
form))
(otherwise `(expt ,arg 2)))))
;; => SQUARE
(square (square 3))
;; => 81
view raw gistfile1.cl hosted with ❤ by GitHub

squareは単にxを二乗する関数だが、ここでは同じ名前のコンパイラマクロを定義している。このコンパイラマクロは、引数のxがアトムだった場合、(expt x 2)へと展開し、(square (square x))という式の場合、(expt x 4)へと展開する。(square (expt x y))という式は(expt x z)になる。それ以外の式は展開されない。つまり、そのまま関数squareが呼ばれる。

また、squareという関数は、コンパイラマクロとは別に存在するため、mapcarのような高階関数でもsquareは普通に使える。

(mapcar #'square '(0 1 2 3 4))
;; => (0 1 4 9 16)
view raw gistfile1.cl hosted with ❤ by GitHub

コンパイラマクロが、同じ動作をする通常のマクロより優れた点はここだ。通常のマクロでもコンパイラマクロと同じようなことはできるが、通常のマクロは高階関数には渡せない。コンパイラマクロと関数の組み合わせは、利用する側のコードからは、普通の関数と全く同じように扱える。

コンパイラマクロは、非常に強力な武器になる。どれほど時間がかかる計算でも、与えられるパラメータが定数であれば、コンパイルするときに定数に置き換えてしまえる。単なるマクロだから、ボトルネックのパターンだけ、高速なコードに展開することも容易だ。

ただ、メリットに応じたデメリットもある。コンパイラマクロは人が行う手動の最適化なので、元の関数が変更されて動作が変わった場合、コンパイラマクロも同じ動作をするように、人が手動で変更しなくてはならない。保守の手間が増えるため、度が過ぎた濫用、早過ぎる最適化は、自分の首を絞めかねない。気を付ける必要がある。

普段はあまり日の目を見ないコンパイラマクロ。性能上のボトルネックに悩んでいる場合、ひとつの選択肢として検討してみて欲しい。

以下参考文献。

0 件のコメント: