Emacs 28.1 with Native compilation (GccEmacs) on macOS Monterey

Emacs 28.1のネイティブコンパイル版をmacOS Montereyで試す

Fish on a bicycle at the Guinness Factory, Dublin

このブログをサボっている間にMacBook Pro (16-inch, 2021, Apple M1 Max)に買い替えたのだが,そのおかげでEmacsがますます速くなった.同じinit.elで起動しても,明らかにMacBook Pro (16-inch, 2019)よりも素早く起動する.そこで,更なる高速化を求め,2020年頃からEmacs界隈を騒がせているネイティブコンパイルなるもの(GccEmacs)をやってみたので,その顛末を後日の自分のためにまとめておくことにした.

References

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 Emacslibgccjit (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

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の起動,ファイルのオープン,文書作成,コード補完などの体感速度は確かに速くなっている気がする.

Avatar
taipapa
本人ではありません (^^)

Related

Previous
comments powered by Disqus