emacsのNative compilation機能(elispのネイティブコンパイル)を試してみる

せっかくの祝日(昭和の日)なので、日がな一日インターネットをさまよっていると、以下のblogに巡り合いました。4/26にemacsのmasterに `feature/native-comp` ブランチがマージされ、Native compilation機能が利用可能になったという記事です。

 * [ネイティブコンパイルEmacsの登場](https://blog.tomoya.dev/posts/hello-native-comp-emacs/)

Native compilation機能...？と思ったのですが、どうやらelispをネイティブバイナリにコンパイルして実行できる機能のようです。たしかにelispは `.el` を `.elc` にバイトコンパイルできることは知っていましたが、今回投入された機能ではネイティブバイナリに変換するものなので、より高速化されるはずです。

これは興味を惹かれる機能なので、さっそく試してみました。

## FreeBSDでNative compilation機能が入ったemacsをビルドする

今回はFreeBSD環境でNative compilation機能が入ったemacsをビルドしてみます。
FreeBSDのバージョンは `12.2-RELEASE` を利用します。

```sh
$ freebsd-version -ku
12.2-RELEASE
12.2-RELEASE
```

### 必要なパッケージのインストール

emacsをビルドするため、事前に以下のパッケージをインストールしておきます。

```sh
$ sudo pkg install gcc11-devel curl autotools gmake texinfo
```

### libgccjit.soがインストールされているか確認

先述のblog記事では、 `libgccjit` がインストールされていることが前提という話が書かれているので、ビルド環境に `libgccjit.so` が存在しているかもあらかじめ確認します。

```sh
$ pkg list gcc11-devel | grep libgccjit.so
/usr/local/lib/gcc11/libgccjit.so
/usr/local/lib/gcc11/libgccjit.so.0
/usr/local/lib/gcc11/libgccjit.so.0.0.1
```

### emacsのビルド

emacsをビルドします。今回は[Merge branch 'feature/native-comp' into into trunk](https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=289000eee729689b0cf362a21baa40ac7f9506f6)のコミット時点のソースツリーを使用してみます。

ポイントは `./configure --with-native-compilation` を指定することで、それ以外はビルド環境に併せて必要なオプションを取捨選択すると良いかと思います。

```sh
$ curl -sLO https://git.savannah.gnu.org/cgit/emacs.git/snapshot/emacs-289000ee\
e729689b0cf362a21baa40ac7f9506f6.tar.gz
$ tar zxf emacs-289000eee729689b0cf362a21baa40ac7f9506f6.tar.gz
$ cd emacs-289000eee729689b0cf362a21baa40ac7f9506f6
$
$ ./autogen.sh
$
$ # configureを実行。
$ # "--with-native-compilation"を指定する。
$ CC=gcc11 \
  CFLAGS='-I/usr/local/include -L/usr/local/lib -L/usr/local/lib/gcc11' \
  LDFLAGS='-L/usr/local/lib -L/usr/local/lib/gcc11' \
  LIBS='-L/usr/local/lib -L/usr/local/lib/gcc11' \
  ./configure \
    --prefix=/opt/emacs \
    --with-native-compilation \
    --without-ns \
    --with-gnutls=ifavailable
```

あとはビルドするだけです。
(家のマシンは古いため、ビルドに大分時間がかかっています...)

```sh
$ time gmake
...
  GEN      info/dir

real    159m5.748s
user    150m27.096s
sys     2m51.316s
$
$ sudo gmake install
```

これでNative compilation機能が組み込まれたemacsのビルドとインストールが完了しました。

## Native compilation機能を試してみる

### Native compilation機能が有効化されていることを確認する

インストールしたemacsを起動し、 `*scratch*` バッファで以下を実行します。

```lisp
(describe-variable 'system-configuration-features)
```

`*Help*` バッファ内の"Its value is"の中に"NATIVE_COMP"が含まれていればネイティブコンパイル機能が有効化されています。

```
Its value is
"ACL ... NATIVE_COMP ... ZLIB"
```

### 簡単なサンプルを動かしてみる

簡単なサンプルでNative compilation機能を試してみます。
まずは `*scratch*` バッファで以下を実行し、必要なライブラリをrequireします。

[elisp-benchmarks](https://elpa.gnu.org/packages/elisp-benchmarks.html)パッケージがNative compilation機能に対応しているらしく、このコードを参考にしてNative compilation機能の使い方を調べてみます。

```lisp
(if (featurep 'nativecomp)
    (require 'comp))
```

Native compilation機能は `(native-compile <関数>)` でネイティブコンパイルできるようです。

```lisp
(native-compile
 '(lambda ()
    (insert "Hello,World.\n")))
#<subr --anonymous-lambda>
```

コンパイルしたバイナリのシンボルが返されるので、それを実行すればOKです。

```lisp
(--anonymous-lambda)
Hello,World.
nil
```

### 簡単に測定してみる

ネイティブコンパイルの方法は把握できました。もう一歩進んだサンプルとして、どれくらい早くるのか簡単なサンプルで測定してみます。

以下のような単純なカウントアップだけする関数を用意します。

```lisp
(defun countup-sample()
  (insert (format-time-string "%s\n"))
  (let ((sum 0) (count 1))
    (while (<= count 10000000)
      (setq sum (+ sum 1))
      (setq count (+ count 1))))
  (insert (format-time-string "%s\n")))
```

単に関数を実行してみます。私の環境では16秒ほどかかるようです。

```lisp
(countup-sample)
1619700318)
1619700334
nil

; before
(- 1619700334 1619700318)
16
```

次に関数をネイティブコンパイルして実行してみます。すると1秒で処理が完了しています。
実行時間が16秒が1秒になっています！単純な関数だけでもかなり速度が向上するようです。

```lisp
(native-compile 'countup-sample)
#<subr countup-sample>

(countup-sample)
1619700363
1619700364
nil

; after
(- 1619700364 1619700363)
1
```

![emacs-native-build.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61137/a71c01ba-520d-d12d-7096-a504a2016d40.png)

## ネイティブコンパイルされたバイナリを見てみる

[elisp-benchmarks](https://elpa.gnu.org/packages/elisp-benchmarks.html)パッケージはネイティブコンパイル機能に対応しており、 `M-x package-install RET elisp-benchmarks RET` でベンチマークを実行すると、開始時にネイティブコンパイルが走ります。

生成されたバイナリは `~/.emacs.d/eln-cache/` 以下に置かれるようです。バイトコンパイルされたelispは `.elc` で、ネイティブコンパイルされたバイナリは `.eln` という拡張子が付与されるようです。

```sh
$ find .emacs.d/ | grep \\.eln | head -n4
.emacs.d/eln-cache/28.0.50-9630aa77/japan-util-36f5bfe8-60d4d083.eln
.emacs.d/eln-cache/28.0.50-9630aa77/cconv-3b1f1f98-ac4628a4.eln
.emacs.d/eln-cache/28.0.50-9630aa77/bytecomp-12882072-b6a69861.eln
```

ファイルを見ると、ちゃんと(？)ELFバイナリになっています。

```sh
$ file .emacs.d/eln-cache/28.0.50-9630aa77/japan-util-36f5bfe8-60d4d083.eln
.emacs.d/eln-cache/28.0.50-9630aa77/japan-util-36f5bfe8-60d4d083.eln: ELF 64-bit LSB shared object, x86-64, version 1 (FreeBSD), dynamically linked, with debug_info, not stripped
$
$ hexdump -C .emacs.d/eln-cache/28.0.50-9630aa77/japan-util-36f5bfe8-60d4d083.eln | head 
00000000  7f 45 4c 46 02 01 01 09  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 3e 00 01 00 00 00  40 0b 00 00 00 00 00 00  |..>.....@.......|
00000020  40 00 00 00 00 00 00 00  60 68 00 00 00 00 00 00  |@.......`h......|
00000030  00 00 00 00 40 00 38 00  05 00 40 00 1e 00 1d 00  |....@.8...@.....|
00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000060  14 24 00 00 00 00 00 00  14 24 00 00 00 00 00 00  |.$.......$......|
00000070  00 00 20 00 00 00 00 00  01 00 00 00 06 00 00 00  |.. .............|
00000080  18 24 00 00 00 00 00 00  18 24 20 00 00 00 00 00  |.$.......$ .....|
00000090  18 24 20 00 00 00 00 00  c4 30 00 00 00 00 00 00  |.$ ......0......|
$
$ readelf -a .emacs.d/eln-cache/28.0.50-9630aa77/japan-util-36f5bfe8-60d4d083.eln | head
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 09 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            FreeBSD
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices x86-64
  Version:                           0x1
```

## まとめ

emacsに搭載されたNative compilation機能を試してみました。簡単なサンプルでもぐっと高速化されるようなので、個人的に普段使いしている[Mew](https://www.mew.org/ja/)や[mhc](https://www.quickhack.net/mhc/)でもネイティブコンパイルを試してみたいところです。

## 参考URL

 * [Just-In-Time Compilation (libgccjit.so)](https://gcc.gnu.org/wiki/JIT)
 * [libgccjit](https://gcc.gnu.org/onlinedocs/jit/)
 * [libgccjit 12.0.0 Tutorial](https://gcc.gnu.org/onlinedocs/jit/intro/index.html)
   * [Tutorial part 1: “Hello world”](https://gcc.gnu.org/onlinedocs/jit/intro/tutorial01.html)