このブログをサボっている間にMacBook Pro (16-inch, 2021, Apple M1 Max)に買い替えたのだが,そのおかげでEmacsがますます速くなった.同じinit.elで起動しても,明らかにMacBook Pro (16-inch, 2019)よりも素早く起動する.そこで,更なる高速化を求め,2020年頃からEmacs界隈を騒がせているネイティブコンパイルなるもの(GccEmacs)をやってみたので,その顛末を後日の自分のためにまとめておくことにした.
References
- ネイティブコンパイルEmacsの登場|日々,とんは語る。
このサイトは必読.libgccjitのエラーについても詳細に書かれている. - [macOS Monterey] ClangからGCCへの切り替え
Emacsのネイティブコンパイルはgccで行うのだが,Mac標準のコンパイラはClangなので,入れ替える必要がある.このサイトにはその際の注意点などが詳細に書かれており助かる. - homebrewでgccをインストールする
gccに入れ替えた後のシンボリックリンクの貼り直しについて詳しく記載されている. - native-comp support #274
私はrailwaycatさんのemacs-macをhomebrewでインストールしているのだが,GitHubでのnative-compilationに関するやりとりが参考になる. - Upgrading to Emacs 28.0 for native compilation
少し古いが,多くのエラーに対するwork aroundが書かれている. - gccemacs
gccemacsの作者であるAndrea Corallo氏のサイト - GccEmacs
EmacsWikiのページ - System Crafters Live! - Emacs Native Comp will change everything • Trying out Eglot • Q&A
ご存じSystem Crafters Liveのネイティブコンパイルに関するYouTube
Preparation
GccEmacsでは,hogehoge.elをgccでコンパイルして,hogehoge.elnというバイナリファイルを作成(これをネイティブコンパイルと呼ぶ)して読み込むことで高速化する.従って,macOSのデフォルトのコンパイラであるClangをgccと入れ替えるなどのいくつかの準備作業が必要となる.
Installation of gcc
これは, [macOS Monterey] ClangからGCCへの切り替えに書いてある通りにやれば良い.まず,gccをインストールする.homebrewのコマンド一発でOK.
$ brew install gcc
$ brew info gcc
gcc: stable 12.1.0 (bottled), HEAD GNU compiler collection
https://gcc.gnu.org/
/opt/homebrew/Cellar/gcc/12.1.0 (1,454 files, 261.6MB) *
Poured from bottle on 2022-08-13 at 21:53:59
.....
これにより,/opt/homebrew/Cellar/にgccがdirectoryとしてインストールされ,自動的に/opt/homebrew/bin/からシンボリックリンクが貼られる.しかし,このままではgccと叩いても,元々のClangの方が呼び出されてしまうので,今回インストールしたgccが呼び出されるようにシンボリックリンクを貼って切り替える.
$ ln -fs /opt/homebrew/bin/gcc-12 /opt/homebrew/bin/gcc
$ ln -fs /opt/homebrew/bin/g++-12 /opt/homebrew/bin/g++
whichコマンドで確認してみる.
$ which gcc
/opt/homebrew/bin/gcc
$ which g++
/opt/homebrew/bin/g++
これでインストールしたgccが呼び出されるようになったことが確認できた.
Installation of libgccjit
GCC Emacs はlibgccjit (jitは just in time の略)と呼ばれるライブラリを使い,GCC (GNU Compiler Collection)のコンパイル機構を用いてオンディマンドにEmacs lispをネイティブな機械語に翻訳する.これにより大きなパフォーマンスの向上が提供される.このためGCC Emacsをbuildするためにはlibgccjitのインストールが必要であり,これもhomebrewで行った.
$ brew install libgccjit
$ brew info libgccjit
libgccjit: stable 12.1.0 (bottled), HEAD
JIT library for the GNU compiler collection
https://gcc.gnu.org/
/opt/homebrew/Cellar/libgccjit/12.1.0 (15 files, 37.7MB) *
Poured from bottle on 2022-08-13 at 21:57:00
.....
Test of gcc compilation with libgccjit
これでgccとlibgccjitを用いて簡単なコードをコンパイルできるかテストした.ネイティブコンパイルEmacsの登場|日々,とんは語る。で記載されているように,Tutorial part 1: “Hello world”にあるコードをhello-world.cとして保存してビルドしてみた.
$ gcc hello-world.c -o hello-world -lgccjit
hello-world.c:20:10: fatal error: libgccjit.h: No such file or directory
20 | #include <libgccjit.h>
| ^~~~~~~~~~~~~
compilation terminated.
そうするとlibgccjit.hがないと怒られてしまう.ヘッダファイルのある場所を指定しないといけない.そこで,以下のようにヘッダファイルの場所を指定すると
$ gcc -I /opt/homebrew/Cellar/libgccjit/12.1.0/include/ hello-world.c -o hello-world -lgccjit
ld: library not found for -lgccjit
collect2: error: ld returned 1 exit status
今度はlibgccjitのライブラリがないと怒られた.
そこで,LIBRARY_PATH環境変数を定義してみた.具体的には,home directoryの.zshenvに以下のように書き込んだ.
$ echo $(brew --prefix libgccjit)
/opt/homebrew/opt/libgccjit # Confirm prefix directory
$ cd ~
$ vi .zshenv
export PATH
export MANPATH
export LIBRARY_PATH=/opt/homebrew/opt/libgccjit/lib/gcc/12
これでコンパイルし直してみると
$ gcc -I /opt/homebrew/Cellar/libgccjit/12.1.0/include/ hello-world.c -o hello-world -L /opt/homebrew/opt/libgccjit/lib/gcc/12/ -lgccjit
$ ./hello-world
hello world
今度はちゃんとコンパイルできて,“hello world"と表示されるようになった.これで準備が整った.
Installation of GCC Emacs
Emacs自体のインストールは How to install Emacs & LaTeX to MacBook Pro 16-inch on Catalina に書いた通りにhomebrewで,railwaycatさんのemacs-macをインストールすれば良いのだが,ネイティブコンパイルを可能にするために –with-native-comp オプションをつけておく.なお,–with-xwidgets –with-rsvgなどはお試しでつけているので無くても良い.
$ brew tap railwaycat/emacsmacport
$ brew install emacs-mac --with-modern-icon --with-imagemagick --with-xwidgets --with-rsvg --with-native-comp
$ brew info emacs-mac
Warning: Treating emacs-mac as a formula. For the cask, use railwaycat/emacsmacport/emacs-mac
railwaycat/emacsmacport/emacs-mac: stable emacs-28.1-mac-9.0, HEAD
YAMAMOTO Mitsuharu's Mac port of GNU Emacs
https://www.gnu.org/software/emacs/
/opt/homebrew/Cellar/emacs-mac/emacs-28.1-mac-9.0 (4,260 files, 151.4MB) *
Built from source on 2022-08-14 at 15:07:27 with: --with-rsvg --with-native-comp --with-xwidgets --with-modern-icon --with-imagemagick
From: https://github.com/railwaycat/homebrew-emacsmacport/blob/HEAD/Formula/emacs-mac.rb
.....
これで,/opt/homebrew/Cellar/emacs-mac/emacs-28.1-mac-9.0/Emacs.appがインストールされる.これを/Application directoryにコピーすれば良い (GNU Emacs 28.1 (build 1, aarch64-apple-darwin21.5.0, Carbon Version 165 AppKit 2113.5)).
Confirmation of native compilation feature activation
Emacsを起動して, C-h f system-configuration-features とすると,下図のようにHelp buffer内の"Its value is"の中に"NATIVE_COMP"が含まれていればネイティブコンパイル機能が有効化されている.
Verification of speedup by native compilation in GCC Emacs
これでGccEmacsがインストールされたわけだが,やはり,どの程度高速化しているのかを知りたいものである.そこで,ネイティブコンパイルによりどれくらい高速化するのか確認するために,ベンチマークを実行してみた.
References
- elisp-benchmarks Home
- elisp-benchmarks Github
- Emacs Native Comp is going to change everything
- gccemacs
- EmacsLispBenchmark
- emacsのNative compilation機能(elispのネイティブコンパイル)を試してみる
- native-comp-elisp-benchmarks — submit a PR with your own benchmark results!
Benchmarks
Emacs Native Comp is going to change everything を参考にバブルソートのベンチマークを以下のようにやってみた.具体的には,scratch bufferに以下のelispを貼って最後の “)” の後でC-jと打って評価するか,org-modeで#+begin_src emacs-lisp #+end_srcの間にelispを挟んでC-x C-eで評価するかのどちらかを行う.最後の3つの数値はそれぞれ経過時間,garbage collectsの数とその時間である.
Interpreted Emacs Lisp
(benchmark-run 3000
(let* ((list (mapcar 'random (make-list 100 most-positive-fixnum)))
(i (length list)))
(while (> i 1)
(let ((b list))
(while (cdr b)
(when (< (cadr b) (car b))
(setcar b (prog1 (cadr b)
(setcdr b (cons (car b) (cddr b))))))
(setq b (cdr b))))
(setq i (1- i)))
list))
(5.813263999999999 2 0.1648019999999999)
Byte compilation
(benchmark-run-compiled 3000
(let* ((list (mapcar 'random (make-list 100 most-positive-fixnum)))
(i (length list)))
(while (> i 1)
(let ((b list))
(while (cdr b)
(when (< (cadr b) (car b))
(setcar b (prog1 (cadr b)
(setcdr b (cons (car b) (cddr b))))))
(setq b (cdr b))))
(setq i (1- i)))
list))
(1.1243109999999998 3 0.24839000000000055)
Native compilation
(setq dw/compiled-benchmark
(native-compile
(lambda ()
(let* ((list (mapcar 'random (make-list 100 most-positive-fixnum)))
(i (length list)))
(while (> i 1)
(let ((b list))
(while (cdr b)
(when (< (cadr b) (car b))
(setcar b (prog1 (cadr b)
(setcdr b (cons (car b) (cddr b))))))
(setq b (cdr b))))
(setq i (1- i)))
list))))
(setq comp-speed 2)
(benchmark-run-compiled 3000
(funcall dw/compiled-benchmark))
(0.796706 3 0.24380600000000108)
ということで,elisp, byte-compilation, native compilationの順に,経過時間は,5.81 s > 1.12 s > 0.797 sと短縮していることがわかる.byte-compilationの効果が凄いな.....
elisp-benchmarks
ネイティブコンパイルに対応したベンチマークのコレクションである.Emacsにおけるネイティブコンパイルを可能にしたAndrea Corallo氏自ら作成したものであるが,こちらもやってみた.
Installation
preludeを使用している場合は,以下を~/.emacs.d/personal/init.orgに追記すれば良い.それ以外の場合は,init.elにuse-package以下の部分を書き込めば良い.
#+begin_src emacs-lisp
(use-package elisp-benchmarks
:ensure t)
#+end_src
Benchmarks
上記インストール後に,M-x elisp-benchmarks-run と打ってテストを開始する.しかし,以下のようなエラーが出てうまくいかない.
emacs-develのelisp-benchmarksに
`bytecomp`, `scroll`, and `smie` are benchmarks I added yesterday which are not microbenchmarks and thus hopefully reflect “real use”.
とあったので,これらの後で追加されたベンチマーク(elb-bytecomp, elb-scroll, elb-smie)をelisp-benchmarks/benchmarksから除去してから再度M-x elisp-benchmarks-runすると,今度はうまく動いてくれた.
Results
test | non-gc avg (s) | gc avg (s) | gcs avg | tot avg (s) | tot avg err (s) |
---|---|---|---|---|---|
bubble | 0.85 | 0.15 | 1 | 1.00 | 0.00 |
bubble-no-cons | 1.42 | 0.00 | 0 | 1.42 | 0.00 |
dhrystone | 1.95 | 0.00 | 0 | 1.95 | 0.01 |
eieio | 1.27 | 0.24 | 3 | 1.51 | 0.00 |
fibn | 0.00 | 0.00 | 0 | 0.00 | 0.00 |
fibn-named-let | 0.83 | 0.00 | 0 | 0.83 | 0.00 |
fibn-rec | 0.00 | 0.00 | 0 | 0.00 | 0.00 |
fibn-tc | 0.01 | 0.00 | 0 | 0.01 | 0.00 |
flet | 1.75 | 0.00 | 0 | 1.75 | 0.00 |
inclist | 1.27 | 0.00 | 0 | 1.27 | 0.00 |
inclist-type-hints | 1.20 | 0.00 | 0 | 1.20 | 0.00 |
listlen-tc | 0.16 | 0.00 | 0 | 0.16 | 0.00 |
map-closure | 6.19 | 0.00 | 0 | 6.19 | 0.00 |
nbody | 1.32 | 0.26 | 1 | 1.57 | 0.00 |
pack-unpack | 0.29 | 0.00 | 0 | 0.29 | 0.00 |
pack-unpack-old | 0.46 | 0.08 | 1 | 0.54 | 0.00 |
pcase | 1.84 | 0.00 | 0 | 1.84 | 0.00 |
pidigits | 4.26 | 2.02 | 7 | 6.29 | 0.02 |
total | 25.05 | 2.75 | 13 | 27.80 | 0.02 |
このような結果が得られた.non-gc avg (s)がバイトコンパイルの結果,gc avg (s)がネイティブコンパイルの結果のようであるが,totalで後者が9.1倍も速いということになる.かなりの高速化であり,本当かと突っ込みたくなるのが正直な感想である.しかし,Emacsの起動,ファイルのオープン,文書作成,コード補完などの体感速度は確かに速くなっている気がする.