2011-01-26

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

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

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

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

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

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

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

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

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

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

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

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

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

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

以下参考文献。

0 件のコメント: