Emacs
elisp
高速化
起動時間短縮
EmacsDay 15

Emacsのスタートアップを視覚的に理解する

More than 3 years have passed since last update.

はじめに

Emacsの起動時間は長い、とよく言われます。

筆者は昨年にVimからEmacsに移行してきたのですが、カスタマイズを重ねる内に実際とても長く感じられるようになりました。emacs -Q として設定ファイルを読み込ませないで起動すると瞬時に起ち上がるので、やはり主な原因はカスタマイズによって増えたElispファイルのロードだろうと考えられます。Emacsの初期化に掛かっている時間は M-x emacs-init-time とすると分かります。著者の環境ではかつて、普通に起動すると "2.7 seconds" と表示されていました。emacs -Qとして起動した場合には "0.3 seconds" と表示されます。つまり、実に2.4秒程度はカスタマイズにより遅くなっていることになります。

Vimでもプラグインを沢山インストールしていましたが、それでもVimの起動が遅いと感じたり、待ったことはありませんでした。カスタマイズ性の高さを誇るEmacsで、カスタマイズする程遅くなるというのは何とも悔しいです。この2.4秒をより短かくするにはどうすれば良いでしょうか?

高速化手段

解決にはいくつか方法があります。

emacsclient

一番速くなるのは emacsclient を使うという方法で、これはそもそもEmacsを起動しない、立ち上げっぱなしにしておくという方法です。

確かに一回Emacsをサーバーとして立ち上げておけば、クライアントの起動は一瞬で終わるので、起動時間で待たされることはなくなります。しかしこれは次善策であって、実際に起動を高速化する方法ではありませんし、また使い勝手も変わってしまいます。

バイトコンパイル

.elファイルを事前にバイトコンパイルしておく方法で、これはもう常套手段ですね。

実際にどのくらい高速化されるのか計測したことはありませんが、コンパイルしたところでロードするファイル数に変化はないと思うので、本質的な解決策にはならないでしょう。というか、バイトコンパイルした状態で2.7秒なので、明らかに不十分です。

autoload & eval-after-load

起動時にロードする.elファイルの数を減らす方法です。
特定のイベントが起きるまで、関連する.elファイルのロードを遅延させるのが基本的な方針になります。autoloadは指定する関数が呼び出された時点、eval-after-loadは指定する.elファイルがロードまたはfeatureがprovideされた時点まで遅らせます。
起動時に集中してロードせずに、Emacsのセッションにロード時間を分散できるため、この方法が最も効果的だろうと思います。

今回はこの方法に焦点を当てます。

スタートアップの視覚化

autoloadやeval-after-loadを使って余計なロードを省くことができれば、大きく起動時間を短縮できるはずです。しかし、実際にロードに時間の掛かっている.elファイルやfeatureが分からなければ、この方法を実践するのは難しいでしょう。

そこで、Emacsの起動中に呼び出される関数の実行時間を計測して、それをインタラクティブなSVGイメージにより可視化するElisp initchart.el を作成しました。計測対象の関数を適当に指定してやれば、最終的に次のようなチャートを吐かせることができます。




(リンク先に飛んで実際に触ってみてね!)

図では横軸が時間に対応していて、Emacsの初期化が終わるまでの期間が表示されています。縦軸は関数の実行期間の入れ子を表現しています。例えば、図中の一番上、1段目の長方形はEmacsの初期化実行期間を表わし、2段目にある長方形は1段目の期間内で呼び出された関数の実行期間を表します。同様に3段目には2段目の関数の実行期間内で呼び出された関数の実行期間に対応する長方形が描かれています。長方形に色は期間の長さに対応します。長ければ長い程、赤く表示されます。また、マウスカーソルを長方形の上に重ねると、ツールチップで関数名(と指定した仮引数に渡された実引数)が表示されます。

この図を使えば、

  • 初期化で時間の掛かっている関数実行はどれか?
  • 何故その関数で時間が掛かっているのか?
  • 望まない関数の実行が発生していないか?

といった事項を視覚的に、そして直感的に確認することができます。

使い方

initchart.elをパスの通ったディレクトリに配置し、init.elの先頭でinitchart.elrequireします。続けてEmacsの初期化処理時に記録したい関数を指定します。

今回は「起動時にロードする.elファイルの数を減らす」ことが目的ですので、.elファイルをロードするような関数を指定します。例えば以下のようにします。

(require 'initchart)
(initchart-record-execution-time-of load file)
(initchart-record-execution-time-of require feature)

この例の場合、load関数とrequire関数の呼び出しを記録するように指示しています。第2引数のfilefeatureは、それぞれの関数の仮引数の内、どの実引数を記録に含めるかを指定します。ここではloadfile仮引数に渡されたファイル名や、requirefeature仮引数に渡されたfeature名をそれぞれ記録するように指示しています。

設定を済ませてからEmacsを起動すると、*initchart*バッファに指定した関数の呼び出し記録が格納されています。
M-x initchart-visualize-init-sequenceとすることで、このバッファの内容を基にして先程のようなSVGファイルを作成できます。

仕組み

関数実行期間の記録にはaroundアドバイスを利用しています。initchart-record-execution-time-ofマクロが指定した関数に対して、実行の前後で時刻の記録をするようなaroundアドバイスを作成し有効化しています。Emacsの初期化時にはこのアドバイスが働き、*initchart*バッファに時刻が記録されます。Elispの実行がシングルスレッドであるので、関数の実行期間は必ず入れ子になるはずです。そこで実行前後の時刻の大小を調べれば、関数呼び出しの入れ子関係を再構築できます。initchart-visualize-init-sequence`コマンドがこの再構築とSVGの作成を行います。

まとめ

Emacsの起動は遅くなりがちですが、それを高速化する手段としてautoloadeval-after-loadのような仕組みが用意されています。これらを有効に活用するためのElispinitchart.elを作成しました。これを使うことで、どの.elファイルのロードに時間が食われているのかが視覚的に理解できるようになりました。

initchart.elを作成した当時(1年くらい前)とは利用しているElisp達も変わってはいますが、initchart.elを利用して起動時間の短縮を進めた結果、現在の著者の環境では M-x emacs-init-time は "1.4 seconds" と表示されるまでになり、およそ半分にまで短縮されました。

皆様も、今年の大掃除ではinitchart.elを使ったEmacsの起動高速化を試されてはいかがでしょうか?