Chapter 07

その他のテクニック:コンパイル時計算

zk_phi
zk_phi
2020.09.24に更新

init.el 内に登場する純粋な (副作用のない) 計算はコンパイル時に行ってしまうことでわずかですが高速化できます。

単純なコンパイル時計算

たとえば重い (しかしキャッシュしても問題ない) 計算 omoi-keisan によって定数 my-super-constant の値が決定される場合:

(defconst my-super-constant (omoi-keisan))

これをコンパイル時計算することで起動を高速化できます。

(defconst my-super-constant
  (eval-when-compile (omoi-keisan)))

長いので適当な短い別名を割り当てておくと軽率に使えて良いと思います。起動時に計算し直す必要がないものには片っ端からつけましょう。

(defalias '! 'eval-when-compile)
(defconst my-super-constant (! (omoi-keisan)))

コンパイル時ループアンローリング

init.el の中に dotimes, dolist など典型的な形のループ処理があって、かつループの範囲が静的に決まっている場合は、アンローリングしてしまった方が変数束縛などのコストがない分、効率がいいです。

-(dolist (cmd '(narrow-to-region
-               dired-find-alternate-file
-               upcase-region
-               downcase-region))
-  (put cmd 'disabled nil))
+(put 'narrow-to-region 'disabled nil)
+(put 'dired-find-alternate-file 'disabled nil)
+(put 'upcase-region 'disabled nil)
+(put 'downcase-region 'disabled nil)

とはいえループの範囲が広い場合や、ループの中で行う処理が複数行にわたるような場合、やっぱり同じコードをたくさんコピペするのは気が引けます。そこで、この変換をコンパイル中にしてしまうマクロを用意しておくと便利です。変数名を指定できる必要はあまりないので、私はアナフォリックマクロ風に、 ,it で参照できるようにしています。

;; setup.el より
(defmacro !foreach (list &rest body)
  "Eval BODY for each elements in LIST. The current element can
be referred with `,it'."
  (declare (indent 1))
  `(progn ,@(mapcar
             (lambda (elem)
               (macroexpand-all
                (if (cadr body) `(progn ,@body) (car body))
                `((,'\, . (lambda (&rest body) `',(funcall `(lambda (it) ,@body) ',elem))))))
             (eval list))))

(!foreach '(narrow-to-region
            dired-find-alternate-file
            upcase-region
            downcase-region)
  (put ,it 'disabled nil))

実装が回りくどく見えますが、単純な it への参照だけでなく「it を含む式」もコンパイル時計算で展開できるようになっているためです。

(!foreach '(narrow-to-region
            dired-find-alternate-file
            upcase-region
            downcase-region)
  (message ,(symbol-name it)))

macroexpand-all の第二引数を利用すると、このように一時的にマクロ定義を flet するような使い方ができてごく稀に便利です。ごく稀ですが…。

環境依存バイトコンパイル

コンパイル後のファイル init.elc のポータビリティを諦めれば、すなわち使うマシンごとにいちいちコンパイルすることにすれば、手間と引き換えにさらなるコンパイル時計算ができるようになります。

コンパイル時 load-path 解決

通常、 requireload は変数 load-path に登録されているディレクトリを探索して目当てのパッケージを探します。しかし頻繁にパッケージの置き場を変えることがないのであれば、毎回この探索をするのは無駄です。

一応これらの関数はパッケージの場所をフルパスで指定することもできるのですが、とはいえベタ書きは避けたい気持ちもあります。

そこで、コンパイル時にそのマシンの load-path からパッケージを探索して、結果をキャッシュしておくようなオレオレ load マクロを定義しておくと便利です。

(defmacro my-load (library &rest args)
  (let ((abs (locate-library library)))
    `(load ,abs ,@args)))

マシンごとにコンパイルする必要はありますが、一度コンパイルしてしまえばバイトコードにフルパスが埋め込まれるので、起動時の load-path の探索は不要になります。

コンパイル時条件分岐

同様にバイトコードのポータビリティを諦めることでできるもう一つの最適化は、 OS ごとの設定などの「条件分岐」のコンパイル時計算です。

たとえば次のように OS ごとに設定をディスパッチするようなコード片があったとき:

(if (eq system-type 'windows-nt)
    ...
  ...)

もしどうせコンパイルした時と同じマシンで使うとわかっているなら、起動時に毎回この条件のチェックを行うのは無駄です。

そこで、コンパイル時に中身を展開してしまうような条件分岐マクロを用意しておくと便利です。

(defmacro !if (test then &rest else)
  (declare (indent 2))
  (if (eval test) then `(progn ,@else)))

私は「フォントのインストール状況によってよしなにフォントを選ぶ」設定や、「ファイルが存在する場合だけ読み込む」設定などもこれで書いています。意外と活用できるところがあると思います。