ネイティブコンパイルEmacsの登場

2021-04-27T18:23:04+0900
emacs

日本時間の2021年4月26日午前3時6分、Emacsのmasterブランチにfeature/native-compブランチがマージされました(コミット:Merge branch ‘feature/native-comp’ into into trunk)。これにより、HEADのEmacsをビルドすると、Native compilation機能を兼ね備えたネイティブコンパイルEmacs、通称Gcc Emacsが使えるようになりました。

ネイティブコンパイルEmacsの機能 #

ネイティブコンパイルEmacs(以下、Gcc Emacsと呼びます)は、Andrea Corallo、Luca Nassi、Nicola Mancaの3名によるBringing GNU Emacs to Native Code
という論文で詳細が説明されています。

簡単に説明すると、これまでのEmacsは、Elispの読み込みを早くするため*.elファイルをバイトコンパイルして*.elcというバイトコードファイルを作成して読み込んでいましたが、Gcc Emacsでは*.elファイルをGCCを使ってコンパイル(これの工程をバイトコンパイルに対してネイティブコンパイルと呼んでいます)して*.elnというバイナリファイルを作成して読み込むことで、バイトコードと比べて2.3倍から42倍ほど高速化させたという話です。

ネイティブコンパイルされたElispファイル

ネイティブコンパイルされたElispファイルをEmacsで開いてみると、上図のようにバイナリファイルになっていることがわかります。

Emacsが早くなるという話であれば、飛び付かないわけにはいきません。そこで、早速試しにビルドしてみました。

macOSにおけるGcc Emacsのビルド #

それでは、macOSでGcc Emacsをビルドしてみたいと思います。まずは、必要なライブラリをインストールしてビルド環境を整えましょう。

Windows on WSLの人はEmacsに来たnative compileを試す - ぐるっとぐりっどの記事を参考にしてみてください。

GCCでlibgccjitを使用する #

Gcc EmacsはGCCとEmacsとのインターフェイスにlibgccjitを利用しています。そのため、Gcc Emacsをビルドするにはlibgccjitのインストールが必要です。僕はHomebrewでインストールしました。

$ brew install libgccjit

libgccjitをインストールしたら、libgccjitを利用したコードをビルドできるか確認してみましょう。

Tutorial part 1: “Hello world”に書かれている通りにコードをビルドしてみます。

$ touch tut01-hello-world.c
$ pbpaste > tut01-hello-world.c # ブラウザでコピーして、pbpasteでファイルに書き込む
$ gcc tut01-hello-world.c -o tut01-hello-world -lgccjit
ld: library not found for -lgccjit
clang: error: linker command failed with exit code 1 (use -v to see invocation)

僕の環境では、libgccjitのライブラリが見つけられず、上記のエラーが表示されてしまいました。

そこで、LIBRARY_PATH環境変数を定義してビルドしてみました。

$ export LIBRARY_PATH="$(brew --prefix libgccjit)/lib/gcc/10"
$ gcc tut01-hello-world.c -o tut01-hello-world -lgccjit
$ ./tut01-hello-world
hello world

今度は正常にビルドが成功しました。生成されたtut01-hello-worldバイナリファイルを実行するとhello worldも表示できたので、.zshenvファイルに環境変数LIBRARY_PATHを追加しておきました。

Emacsをビルドする #

Gcc Emacsをビルドするには、--with-native-compilationオプションを付けてビルドします。

ソースからのEmacsをビルドする方法は、Emacsのリポジトリに含まれているINSTALL.REPOファイルに詳しく書かれているので、はじめてビルドする方はぜひ一度確認しておきましょう。

ドキュメントに書かれているように、初回はリポジトリをクローンしてconfigureファイルを生成する必要があります。

$ git clone git://git.sv.gnu.org/emacs.git
$ cd ./emacs
$ ./autogen.sh # configureファイルが作成される

もし、途中でエラーがでたら、エラーメッセージを読んでみましょう。おそらく、automakeとautoconfが足りないだけだと思いますので、brew install autoconf automakeでインストールしてリトライしてみましょう。

configureファイルを生成できたら、必要なオプションを付けてconfigureスクリプトを実行します。僕は次のオプションで実行しました。

$ ./configure --with-native-compilation --with-ns --without-x
(中略)
  Does Emacs have dynamic modules support?                yes
  Does Emacs use toolkit scroll bars?                     yes
  Does Emacs support Xwidgets?                            no
  Does Emacs have threading support in lisp?              yes
  Does Emacs support the portable dumper?                 yes
  Does Emacs support legacy unexec dumping?               no
  Which dumping strategy does Emacs use?                  pdumper
  Does Emacs have native lisp compiler?                   yes
(後略)

最後の方に表示されるメッセージを確認して、Does Emacs have native lisp compiler? yesとなっていれば、--with-native-compilationオプションが有効になっていて、Gcc Emacsをビルドできます。

この後、通常はmake installを実行すればEmacs.appが生成されるのですが、今回は次のようなエラーが出てしまいました。

$ make install
(中略)
make[1]: Nothing to be done for `maybe-blessmail'.
find native-lisp -type d -exec /usr/local/bin/gmkdir -p "/Users/tomoya/.local/share/ghq/git.sv.gnu.org/emacs/nextstep/Emacs.app/Contents/Resources/{}" \; ; \
	find native-lisp -type f -exec /usr/local/bin/ginstall -c -m 644 "{}" "/Users/tomoya/.local/share/ghq/git.sv.gnu.org/emacs/nextstep/Emacs.app/Contents/Resources/{}" \;
find: native-lisp: No such file or directory
find: native-lisp: No such file or directory
make: *** [install-eln] Error 1

こういうときはmake bootstrapを試してみます。

$ make bootstrap
(中略)
  GEN      ../../info/url.info
  GEN      ../../info/vhdl-mode.info
  GEN      ../../info/vip.info
  GEN      ../../info/viper.info
  GEN      ../../info/widget.info
  GEN      ../../info/wisent.info
  GEN      ../../info/woman.info
  GEN      ../../info/efaq-w32.info
  GEN      info/dir

make bootstrapがエラーなしで完了できたので、もう一度make installを実行します。

$ make install
(中略)
make[1]: Nothing to be done for `maybe-blessmail'.
find native-lisp -type d -exec /usr/local/bin/gmkdir -p "/Users/tomoya/.local/share/ghq/git.sv.gnu.org/emacs/nextstep/Emacs.app/Contents/Resources/{}" \; ; \
	find native-lisp -type f -exec /usr/local/bin/ginstall -c -m 644 "{}" "/Users/tomoya/.local/share/ghq/git.sv.gnu.org/emacs/nextstep/Emacs.app/Contents/Resources/{}" \;

すると、今度は無事にmake installも成功して、nextstepディレクトリにEmacs.appが生成されました。

あとは、これを/Applicationsディレクトリに配置すればインストール完了です。

Gcc Emacsを起動する #

ビルドしたGcc Emacsを起動してみましょう。Emacs.appをダブルクリックして起動しても良いですが、僕は環境変数や署名やらのトラブルに巻き込まれないようにするため、ターミナルから次のようにして起動しています。

$ open -a Emacs.app

もちろん毎回コマンドをタイプするのは面倒なので、次のようなエイリアスを追加してEだけで起動するようにしています。

alias E='open -a Emacs.app'

起動したGcc Emacs

Emacsを起動したら、M-x describe-variable RET system-configuration-features RETを実行してみましょう。表示された*Help*バッファのIts value isNATIVE_COMPが含まれていれば、ネイティブコンパイルできるGcc Emacsになります。

Gcc Emacsのパフォーマンス #

Gcc Emacsがどれくらい早いのか、せっかくなのでベンチマークを実行してみます。

GNU ELPAにあるelisp-benchmarksは、ネイティブコンパイルに対応していて、事前にネイティブコンパイルしてからベンチマークを実行します。

というわけで、自分の手元でGcc EmacsとEmacs 27.2でベンチマークを実行してみると次のような結果になりました。

test Gcc Emacs (s) Emacs 27.2 (s) Rel
bubble-no-cons 2.25 4.21 1.9x
bubble 1.46 2.10 1.4x
dhrystone 2.11 4.79 2.3x
fibn-rec 0.00 4.02 N.D.
fibn-tc 0.01 4.30 430.0x
fibn 0.00 5.12 N.D.
flet 2.10 6.30 3.0x
inclist-type-hints 1.04 7.66 7.4x
inclist 1.76 5.86 3.3x
listlen-tc 0.22 4.53 20.6x
map-closure 5.85 5.64 1.0x
nbody 1.36 2.30 1.7x
pcase 2.17 5.35 2.5x
pidigits 8.95 9.14 1.0x
total 29.30 71.34 2.4x

トータルの比較は2.4倍早いという結果になりましたが、fibn-recfibnでは、Gcc Emacsだと0秒になっていて、正直ほんまいかいなという感じです。

個人的な実感として、昨日からコードやブログを書きつつ試したかぎりでは、コード補完や定義ジャンプなど、これまで少し重たいと感じていた処理が、少し早くなった気がするので、Elispの実行速度が早くなっていることは確かな気がします。

また、grugrutさんがEmacsのNative Compilationの性能を測定する - ぐるっとぐりっどでバブルソートによるベンチマークを行っています。

ネイティブコンパイルの実行タイミングと保存場所 #

Gcc EmacsはネイティブコンパイルされていないElispを読み込むタイミングで、非同期でネイティブコンパイルを実行します。そのため、Gcc Emacsの初回起動からしばらくの間は大量のコンパイルが実行され、EmacsのCPUが100%になり操作が重くなるかと思いますが、焦らなくて大丈夫です。コンパイルログは*Async-native-compile-log*に出力されるので眺めてみるとよいでしょう。

ネイティブコンパイルログ

また、ネイティブコンパイル時に*Warnings*バッファにメッセージが表示されることがありますが、これはバイトコンパイルのときのワーニングと基本的には同じで、Elispの開発者でなければ無視して問題ありません。

ちなみに、ネイティブコンパイルされた*.elnファイルは、バイトコンパイルされた*.elcファイルのように、*.elファイルと同じディレクトリに保存されません。

どこに保存されるかと言うと、Emacsに同梱されているElispの*.elnファイルは、/Applications/Emacs.app/Contents/Resources/native-lispに保存され、追加でインストールしたElispは<user-emacs-directory>/eln-cacheディレクトリ(デフォルトであれば~/.emacs.d/eln-cacheディレクトリ)に保存されます。そして、load-pathと同じように、comp-eln-load-pathのリストで定義されているパスから読み取るようになっています。

まとめ #

とりあえず、昨日ビルドしての今日なので、正直まだあまり情報がありませんが、マージされたばかりでも特に問題なく動作しているのはとても素晴しいの一言です。また、macOSは一部の機能がおいてけぼりになることが珍しくないのですが、ネイティブコンパイルが使えてとても嬉しく思います。

ネイティブコンパイルを備えたEmacs 28は、個人的にEmacsの内部文字コードがUTF-8に統一されたEmacs 23以来のビッグリリースだと感じています。ですので、HEADをビルドするのはちょっと難しい人も、Emacs 28がリリースされる日を楽しみに待っていてください。