せっかくの祝日(昭和の日)なので、日がな一日インターネットをさまよっていると、以下のblogに巡り合いました。4/26にemacsのmasterに feature/native-comp
ブランチがマージされ、Native compilation機能が利用可能になったという記事です。
Native compilation機能...?と思ったのですが、どうやらelispをネイティブバイナリにコンパイルして実行できる機能のようです。たしかにelispは .el
を .elc
にバイトコンパイルできることは知っていましたが、今回投入された機能ではネイティブバイナリに変換するものなので、より高速化されるはずです。
これは興味を惹かれる機能なので、さっそく試してみました。
FreeBSDでNative compilation機能が入ったemacsをビルドする
今回はFreeBSD環境でNative compilation機能が入ったemacsをビルドしてみます。
FreeBSDのバージョンは 12.2-RELEASE
を利用します。
$ freebsd-version -ku
12.2-RELEASE
12.2-RELEASE
必要なパッケージのインストール
emacsをビルドするため、事前に以下のパッケージをインストールしておきます。
$ sudo pkg install gcc11-devel curl autotools gmake texinfo
libgccjit.soがインストールされているか確認
先述のblog記事では、 libgccjit
がインストールされていることが前提という話が書かれているので、ビルド環境に libgccjit.so
が存在しているかもあらかじめ確認します。
$ 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のコミット時点のソースツリーを使用してみます。
ポイントは ./configure --with-native-compilation
を指定することで、それ以外はビルド環境に併せて必要なオプションを取捨選択すると良いかと思います。
$ 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
あとはビルドするだけです。
(家のマシンは古いため、ビルドに大分時間がかかっています...)
$ 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*
バッファで以下を実行します。
(describe-variable 'system-configuration-features)
*Help*
バッファ内の"Its value is"の中に"NATIVE_COMP"が含まれていればネイティブコンパイル機能が有効化されています。
Its value is
"ACL ... NATIVE_COMP ... ZLIB"
簡単なサンプルを動かしてみる
簡単なサンプルでNative compilation機能を試してみます。
まずは *scratch*
バッファで以下を実行し、必要なライブラリをrequireします。
elisp-benchmarksパッケージがNative compilation機能に対応しているらしく、このコードを参考にしてNative compilation機能の使い方を調べてみます。
(if (featurep 'nativecomp)
(require 'comp))
Native compilation機能は (native-compile <関数>)
でネイティブコンパイルできるようです。
(native-compile
'(lambda ()
(insert "Hello,World.\n")))
#<subr --anonymous-lambda>
コンパイルしたバイナリのシンボルが返されるので、それを実行すればOKです。
(--anonymous-lambda)
Hello,World.
nil
簡単に測定してみる
ネイティブコンパイルの方法は把握できました。もう一歩進んだサンプルとして、どれくらい早くるのか簡単なサンプルで測定してみます。
以下のような単純なカウントアップだけする関数を用意します。
(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秒ほどかかるようです。
(countup-sample)
1619700318)
1619700334
nil
; before
(- 1619700334 1619700318)
16
次に関数をネイティブコンパイルして実行してみます。すると1秒で処理が完了しています。
実行時間が16秒が1秒になっています!単純な関数だけでもかなり速度が向上するようです。
(native-compile 'countup-sample)
#<subr countup-sample>
(countup-sample)
1619700363
1619700364
nil
; after
(- 1619700364 1619700363)
1
ネイティブコンパイルされたバイナリを見てみる
elisp-benchmarksパッケージはネイティブコンパイル機能に対応しており、 M-x package-install RET elisp-benchmarks RET
でベンチマークを実行すると、開始時にネイティブコンパイルが走ります。
生成されたバイナリは ~/.emacs.d/eln-cache/
以下に置かれるようです。バイトコンパイルされたelispは .elc
で、ネイティブコンパイルされたバイナリは .eln
という拡張子が付与されるようです。
$ 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バイナリになっています。
$ 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やmhcでもネイティブコンパイルを試してみたいところです。