誰でもわかるわけないコンパイラマクロ
Common Lispのソースコードを読むとたまに出てくる、define-compiler-macro。これは何ぞや、と思ったことのある人も多いのではないだろうか。今回のテーマはコンパイラマクロ。
define-compiler-macroとは、その名の通り、コンパイラマクロを定義するマクロのこと。では、コンパイラマクロとは何なのか?
端的に言えば、マクロの一種だ。普通のdefmacroで定義するマクロと何が違うかというと、
- 他の関数やマクロと名前が重複しても良い
- コンパイラマクロを展開する前の式が&whole引数に束縛される
- funcallによる呼び出しも展開される
- 展開しなくてもいい
これを見ても、何に使うのかちょっと予想できないかもしれない。他と名前が重複したら、展開するときに困るだろうし、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は普通に使える。
コンパイラマクロが、同じ動作をする通常のマクロより優れた点はここだ。通常のマクロでもコンパイラマクロと同じようなことはできるが、通常のマクロは高階関数には渡せない。コンパイラマクロと関数の組み合わせは、利用する側のコードからは、普通の関数と全く同じように扱える。
コンパイラマクロは、非常に強力な武器になる。どれほど時間がかかる計算でも、与えられるパラメータが定数であれば、コンパイルするときに定数に置き換えてしまえる。単なるマクロだから、ボトルネックのパターンだけ、高速なコードに展開することも容易だ。
ただ、メリットに応じたデメリットもある。コンパイラマクロは人が行う手動の最適化なので、元の関数が変更されて動作が変わった場合、コンパイラマクロも同じ動作をするように、人が手動で変更しなくてはならない。保守の手間が増えるため、度が過ぎた濫用、早過ぎる最適化は、自分の首を絞めかねない。気を付ける必要がある。
普段はあまり日の目を見ないコンパイラマクロ。性能上のボトルネックに悩んでいる場合、ひとつの選択肢として検討してみて欲しい。
以下参考文献。
- Arthur Lemmens. (Jun 2004). "Compiler macros".
- CLHS: Macro DEFINE-COMPILER-MACRO
0 件のコメント:
コメントを投稿