#:g1: Allegro CL: Hackable LAP codeの紹介

Posted 2014-11-18 15:00:00 GMT

(LISP Library 365参加エントリ)

 LISP Library 365 の323日目です。

Allegro CL: Hackable LAP codeとはなにか

 Allegro CL: Hackable LAP codeは、Allegro CLでコンパイラの出力をいじる仕組みです。Hackable LAP codeというかどうかは分かりませんが、Erik Naggum氏の話の中でこういう表現があるので、とりあえずこう呼ぶことにしてみます。

パッケージ情報

パッケージ名Allegro CL: Hackable LAP code

インストール方法

 Allegro CLに標準の機能です。Allegro CL 4.3でも使えるのでかなり古くから(遅くとも1997年位から)あるようです。

試してみる

 件のHackable LAP codeという呼び方ですが、

に出てくるものです。

 この機能については正式なドキュメントがないようなのですが、ILC 2007のDuane Rettig氏のチュートリアルで解説があったようです。

コンパイラの出力を編集する

 編集の仕方が黒魔術的なのですが、下記の手順で行ないます。
とりあえず、手短なところで与えられた引数をそのまま返すidという関数を定義してみます。

(defun id (obj) obj)

Common Lispのidentityと機能は同じものですが、コンパイル時にフックが掛ってLAPを編集するのでコンパイルはしないで置きます。

 何もしない場合のdisassembleの結果は下記のようになります。

(disassemble #'id)
;>>  ;; disassembly of #<Function ID>
;>>  ;; formals: OBJ
;>>  
;>>  ;; code start: #x1001229d68:
;>>     0: 48 83 f8 01    cmp	rax,$1
;>>     4: 74 01          jz	7
;>>     6: 06             (push es)       ; SYS::TRAP-ARGERR
;>>     7: 41 80 7f a7 00 cmpb	[r15-89],$0 ; SYS::C_INTERRUPT-PENDING
;>>    12: 74 01          jz	15
;>>    14: 17             (pop ss)        ; SYS::TRAP-SIGNAL-HIT
;>>    15: f8             clc
;>>    16: 4c 8b 74 24 10 movq	r14,[rsp+16]
;>>    21: c3             ret
;>>  
;=>  <no values>

 次にコンパイル時にLAPを編集する関数を指定します。

(setq comp::*hack-compiler-output* '(id))

 そしてコンパイルすると、hackit.sができるので、これをエディタで編集します。

(let ((*default-pathname-defaults* #P"/tmp/"))
  (compile 'id))
;>> type :cont when you're done editing "hackit.s"
;>>    [Condition of type SIMPLE-BREAK]
;>> 
;>> Restarts:
;>>  0: [CONTINUE] return from break.
;>>  1: [RETRY] Retry SLIME interactive evaluation request.
;>>  2: [*ABORT] Return to SLIME's top level.
;>>  3: [ABORT] Abort entirely from this (lisp) process.

hackit.sは、こんな感じの内容になっています。

(LABEL GARBAGE::L2)
(CMP.Q (IM 1) (:REG 0 :RAX :EAX :AX :AL))
(BCC :EQ GARBAGE::L3)
(TRAP.WNAERR)
(LABEL GARBAGE::L3)
(LABEL GARBAGE::L1)
(CMP.B (IM 0) (D -89 (:REG 15 :R15 :R15D :R15W :R15B)))
(BCC.S :EQ GARBAGE::L4)
(TRAP.SIGNAL-HIT)
(LABEL GARBAGE::L4)
(CLC)
(MOVE.Q (D 16 (:REG 4 :RSP :ESP :SP :SPL))
        (:REG 14 :R14 :R14D :R14W :R14B))
(RETURN)

大体上のアセンブリの出力と対応しているのが分かります。
これを適当に編集しますが、Allegro CLでは、RAXにアリティが入るようで、これが1個かどうかをチェックしているようです。
試しにこれを削除してみます。

(CLC)
(MOVE.Q (D 16 (:REG 4 :RSP))
        (:REG 14 :R14))
(RETURN)

 これだと何もしてないように見えますが、返り値が置かれるレジスタが第一引数が置かれるレジスタと同じなのでOKです。
なお、レジスタの指定は、番号が重要で、レジスタの名前はコメントのようなので上のように書いても大丈夫みたいです。
編集し終わったら継続してコンパイル完了です。

(id 42 1 38 8)
;=> 42

(disassemble #'id) ;>> ;; disassembly of #<Function ID> ;>> ;; formals: OBJ ;>> ;>> ;; code start: #x1003749a98: ;>> 0: f8 clc ;>> 1: 4c 8b 74 24 10 movq r14,[rsp+16] ;>> 6: c3 ret ;>> 7: 90 nop ;>> ;=> <no values>

引数をチェックしていない関数になりました。

コンパイル時に用意したLAPを結合させる

 これで編集できることは分かったのですが、これを毎度やるのは現実的ではない、ということで、ファイルを読み込ませる方法もあります。

(setq comp::*assemble-function-body* '((id . #P"/tmp/id.s")))

(compile ...)

(setq comp::*assemble-function-body* nil)

LAPを直接編集して高速化した関数の速度計測

 さてidが、identityより速いのか計測してみましょう。

(time
 (dotimes (i 1000000000)
   (id 1)))
; cpu time (non-gc) 4.784000 sec user, 0.000000 sec system
; cpu time (gc)     0.000000 sec user, 0.000000 sec system
; cpu time (total)  4.784000 sec user, 0.000000 sec system
; real time  4.784323 sec
; space allocation:
;  54 cons cells, 5,232 other bytes, 0 static bytes

(time (dotimes (i 1000000000) (identity 1))) ; cpu time (non-gc) 1.276000 sec user, 0.000000 sec system ; cpu time (gc) 0.000000 sec user, 0.000000 sec system ; cpu time (total) 1.276000 sec user, 0.000000 sec system ; real time 1.276108 sec ; space allocation: ; 0 cons cells, 0 other bytes, 0 static bytes

3倍位遅いwwwww
identityより遅い理由ですが、identityにはコンパイラマクロが定義してあって、identity自体がいなくなるので、上記のコードのような場合、identityとしては最速の実装ということになります。
さすがになかなか賢いですね。

 ちなみに、idを(speed 3)(safety 0)で最適化すると引数チェックが消えて上記のLAPコードと同じものになるので、引数チェックを無くすためであれば、わざわざLAPコードを編集する必要はありません。

まとめ

 今回は、Allegro CL: Hackable LAP codeを紹介してみました。
なかなか黒魔術的な機能で良いですね。Allegro CLにはこういうのが他にも沢山あるようです。

comments powered by Disqus