#:g1: Emacs Lispのネイティブコンパイラ vs Common Lisp

Posted 2021-04-28 22:14:11 GMT

今日はSNSでEmacs Lispのネイティブコンパイラがメインラインに来たという話題で賑っていたので、早速GNU Emacsを--with-native-compでビルドして、Common Lispのネイテイブコンパイラと比較してどんなものなのか眺めてみました。
眺めるといっても、お馴染のfibのマイクロベンチを走らせるだけですが。

Emacs Lisp で実行形態三種の比較

まず、Emacs Lispで、通常の定義、バイトコンパイル、ネイティブコンパイルで比較してみます。
いまひとつ作法が分かっていないのですが、定義形式を合せるために、(setf symbol-function)しています。
一応defunでの定義をファイルに書き出してからファイルをコンパイルする手順でも確認してみましたが、結果はほぼ同じようです。

最適化設定が良く分からないのですが、

(setq comp-speed 3)

するとCommon Lispでいう、(declaim (optimize speed 3))的なことになるようなので、以下はこの設定の元で実験しています。

(setf (symbol-function 'fib)
      (lambda (n)
        (if (< n 2)
            n
          (+ (fib (1- n))
             (fib (- n 2))))))

(benchmark-call (lambda () (fib 30)) 1)(0.540617072 0 0.0) (benchmark-call (lambda () (fib 40)) 1)(79.139097209 0 0.0)

(setf (symbol-function 'fib-bc)
      (byte-compile
       (lambda (n)
         (if (< n 2)
             n
           (+ (fib-bc (1- n))
              (fib-bc (- n 2)))))))

(benchmark-call (lambda () (fib-bc 30)) 1)(0.346994376 0 0.0) (benchmark-call (lambda () (fib-bc 40)) 1)(40.036792254 0 0.0)

(setf (symbol-function 'fib-nc)
      (native-compile
       (lambda (n)
         (if (< n 2)
             n
           (+ (fib-nc (1- n))
              (fib-nc (- n 2)))))))

(benchmark-call (lambda () (fib-nc 30)) 1)(0.253148658 0 0.0) (benchmark-call (lambda () (fib-nc 40)) 1)(29.594145706 0 0.0)

大体のところですが、

elisp: (fib 40) Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz
interp 79sec
byte-comp 40sec
native-comp 29sec

という結果でした。

Common Lispとの比較

大体のタイムは分かったので、似たようなスピードのCommon Lisp処理系ということだとCLISPあたりか、ということで、CLISPで関数をコンパイルした場合と、Emacs Lispのネイティブコンパイルのfibの結果を比較してみたところ、両者の結果は、ほぼ同じになりました。
なお、CLISPはネイティブコンパイラではなくバイトコンパイラの処理系です。

(setf (symbol-function 'fib)
      (lambda (n)
        (if (< n 2)
            n
            (+ (fib (1- n))
               (fib (- n 2))))))

(time (fib 30)) Real time: 0.248056 sec. Run time: 0.25 sec. Space: 0 Bytes → 832040

(time (fib 40)) Real time: 30.663303 sec. Run time: 30.59 sec. Space: 0 Bytes → 102334155

CLISP: (fib 40) Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz
interp 116sec
byte-comp 30sec

Common Lispのネイティブコンパイラとの比較

Common Lispのメジャーな処理系は大体ネイティブコンパイラですが、LispWorksで最適化設定なしの場合は、こんな感じです。

(setf (symbol-function 'fib)
      (lambda (n) 
        (if (< n 2)
            n
            (+ (fib (1- n))
               (fib (- n 2))))))

(compile 'fib) Timing the evaluation of (fib 40)

User time = 1.350 System time = 0.010 Elapsed time = 1.293 Allocation = 269016 bytes 0 Page faults → 102334155

LispWorks: (fib 40) Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz
interp 186sec
native-comp 1.35sec

Emacs Lispのネイティブコンパイルfibと比べると大体22倍程度の速さです。
ちなみにLispWorksのインタプリタ実行はかなり遅いことがわかりますが、Common Lisp仕様がコンパイル指向なので、インタプリタはおまけだったり、サポートしていなかったりの処理系が殆どです。

なお、SBCLやLispWorksで型宣言や再帰的なインライン展開を実施して最速を狙うと、ネイティブコンパイルのEmacs Lispの約95倍程度の速度が出ます。
関数呼出しの速度を計測するためのfibのようなマイクロベンチでインライン展開するのは卑怯な気もするのですが、gcc等も展開してくるので、gccに合せるならアリかなと思います。

(defun fib (n)
  (declare (optimize (speed 3) (safety 0) (debug 0) (compilation-speed 0))
           (fixnum n))
  (labels ((fib (n)
             (declare (fixnum n))
             (the fixnum
                  (if (< n 2)
                      n
                      (+ (fib (1- n))
                         (fib (- n 2))))))) 
    (declare (inline fib))
    (fib n)))

CL-USER> (time (fib 40)) Evaluation took: 0.322 seconds of real time 0.320000 seconds of total run time (0.320000 user, 0.000000 system) 99.38% CPU 1,061,775,900 processor cycles 0 bytes consed

まとめ

いまのところEmacs Lispの最適化の作法があまり確立していないようなのですが、Common Lisp風に人が指示する手段もそこそこ用意されているようです。
ただ、まだ発展途上のようでCommon Lispの感覚でコンパイラに最適化のヒント与えようとしても上手く行かない様子。
例えば、comp-hint-fixnumは、(the fixnum ...)な雰囲気のもののようですが、(optimize (speed 3))で使うと現状では却って遅くなったりします。

また、Common Lispではdisassembleで結果を確認しつつ追い込んでいくのが普通ですが、Common Lispのネイティブコンパイラのディスアセンブルの結果と違ってCのコンパイラの世界と行き来している感があります。

Emacs Lispが今後Common Lispのように人力チューニングを中心に展開していくのか、JITを中心にしていくのかは分かりませんが、Common Lisp風なシステムを目指すのであれば、もう少し道具が充実する必要がありそうです。

今回のfibdisassembleの結果は下記のようになります。
なお、現状、AT&T表記決め打ちのようなのですが、disassemble-internalの中でobjdumpを呼んでいるだけのようなので、ここをいじれば好きな表記に変更できるようです。

disassemble-internal:
...
(call-process "objdump" nil (current-buffer) t "-S" "-M" "intel"
                            (native-comp-unit-file (subr-native-comp-unit obj)))
...

0000000000001290 <F6669622d6e63_fib_nc_0>:
    1290:   41 54                   push   r12
    1292:   55                      push   rbp
    1293:   53                      push   rbx
    1294:   48 83 ec 50             sub    rsp,0x50
    1298:   4c 8b 25 49 2d 00 00    mov    r12,QWORD PTR [rip+0x2d49]        # 3fe8 <freloc_link_table@@Base-0x5f8>
    129f:   48 8b 2d 3a 2d 00 00    mov    rbp,QWORD PTR [rip+0x2d3a]        # 3fe0 <d_reloc@@Base-0x580>
    12a6:   49 8b 1c 24             mov    rbx,QWORD PTR [r12]
    12aa:   48 8b 7d 00             mov    rdi,QWORD PTR [rbp+0x0]
    12ae:   ff 93 a8 26 00 00       call   QWORD PTR [rbx+0x26a8]
    12b4:   bf 02 00 00 00          mov    edi,0x2
    12b9:   48 89 e6                mov    rsi,rsp
    12bc:   48 c7 44 24 08 0a 00    mov    QWORD PTR [rsp+0x8],0xa
    12c3:   00 00 
    12c5:   48 89 04 24             mov    QWORD PTR [rsp],rax
    12c9:   ff 93 28 26 00 00       call   QWORD PTR [rbx+0x2628]
    12cf:   48 8b 7d 00             mov    rdi,QWORD PTR [rbp+0x0]
    12d3:   48 85 c0                test   rax,rax
    12d6:   74 18                   je     12f0 <F6669622d6e63_fib_nc_0+0x60>
    12d8:   ff 93 a8 26 00 00       call   QWORD PTR [rbx+0x26a8]
    12de:   48 83 c4 50             add    rsp,0x50
    12e2:   5b                      pop    rbx
    12e3:   5d                      pop    rbp
    12e4:   41 5c                   pop    r12
    12e6:   c3                      ret    
    12e7:   66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
    12ee:   00 00 
    12f0:   ff 93 a8 26 00 00       call   QWORD PTR [rbx+0x26a8]
    12f6:   48 89 c7                mov    rdi,rax
    12f9:   8d 40 fe                lea    eax,[rax-0x2]
    12fc:   a8 03                   test   al,0x3
    12fe:   75 20                   jne    1320 <F6669622d6e63_fib_nc_0+0x90>
    1300:   48 ba 00 00 00 00 00    movabs rdx,0xe000000000000000
    1307:   00 00 e0 
    130a:   48 89 f8                mov    rax,rdi
    130d:   48 c1 f8 02             sar    rax,0x2
    1311:   48 39 d0                cmp    rax,rdx
    1314:   74 0a                   je     1320 <F6669622d6e63_fib_nc_0+0x90>
    1316:   48 8d 04 85 fe ff ff    lea    rax,[rax*4-0x2]
    131d:   ff 
    131e:   eb 0a                   jmp    132a <F6669622d6e63_fib_nc_0+0x9a>
    1320:   49 8b 04 24             mov    rax,QWORD PTR [r12]
    1324:   ff 90 90 25 00 00       call   QWORD PTR [rax+0x2590]
    132a:   48 8b 55 18             mov    rdx,QWORD PTR [rbp+0x18]
    132e:   48 8d 74 24 10          lea    rsi,[rsp+0x10]
    1333:   48 89 44 24 18          mov    QWORD PTR [rsp+0x18],rax
    1338:   bf 02 00 00 00          mov    edi,0x2
    133d:   48 89 54 24 10          mov    QWORD PTR [rsp+0x10],rdx
    1342:   ff 93 e0 1a 00 00       call   QWORD PTR [rbx+0x1ae0]
    1348:   48 8b 7d 00             mov    rdi,QWORD PTR [rbp+0x0]
    134c:   49 89 c4                mov    r12,rax
    134f:   ff 93 a8 26 00 00       call   QWORD PTR [rbx+0x26a8]
    1355:   48 8d 74 24 20          lea    rsi,[rsp+0x20]
    135a:   bf 02 00 00 00          mov    edi,0x2
    135f:   48 c7 44 24 28 0a 00    mov    QWORD PTR [rsp+0x28],0xa
    1366:   00 00 
    1368:   48 89 44 24 20          mov    QWORD PTR [rsp+0x20],rax
    136d:   ff 93 f8 25 00 00       call   QWORD PTR [rbx+0x25f8]
    1373:   48 8b 55 18             mov    rdx,QWORD PTR [rbp+0x18]
    1377:   48 8d 74 24 30          lea    rsi,[rsp+0x30]
    137c:   bf 02 00 00 00          mov    edi,0x2
    1381:   48 89 44 24 38          mov    QWORD PTR [rsp+0x38],rax
    1386:   48 89 54 24 30          mov    QWORD PTR [rsp+0x30],rdx
    138b:   ff 93 e0 1a 00 00       call   QWORD PTR [rbx+0x1ae0]
    1391:   66 49 0f 6e c4          movq   xmm0,r12
    1396:   48 8d 74 24 40          lea    rsi,[rsp+0x40]
    139b:   bf 02 00 00 00          mov    edi,0x2
    13a0:   66 48 0f 6e c8          movq   xmm1,rax
    13a5:   66 0f 6c c1             punpcklqdq xmm0,xmm1
    13a9:   0f 29 44 24 40          movaps XMMWORD PTR [rsp+0x40],xmm0
    13ae:   ff 93 00 26 00 00       call   QWORD PTR [rbx+0x2600]
    13b4:   48 83 c4 50             add    rsp,0x50
    13b8:   5b                      pop    rbx
    13b9:   5d                      pop    rbp
    13ba:   41 5c                   pop    r12
    13bc:   c3                      ret    
    13bd:   0f 1f 00                nop    DWORD PTR [rax]


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus