bytecode
前回の最後にやった、pythonによる、たらい回し(ああ、たらい と言ったら昭 和の匂いがプンプンするな)を32Bitのマシンで再確認。ちょいと気になる点が 有たから。
まずは、オリジナル。1- が無かったので、自前で定義してそれを呼び出すや つ。
debian:__pycache__$ time python tarai.cpython-36.pyc real 0m7.311s user 0m7.220s sys 0m0.008s
64Bit仮想マシン(@FreeBSD 11.2)より、良い成績だ。実機だから? それとも FreeBSDが、保守的だから? それともdebian側で使ったpythonは、アナコンダ 由来のものなんで、蛇の魔力が働いた? 考えだすと切りがないんで、この際置いておく。
気になってたのは、1-の変りに定義したm1を呼んでいる点(移植の 常套手段)。これ、呼び出しコスト がかかる処理のはず。m1使うのやめて、埋めこんじゃえ。かっこよく言えば、 インライン化。コードを曝けだすと、こんな感じ。
(defn tarai [x y z]
(if (<= x y)
y
(tarai (tarai (- x 1) y z) (tarai (- y 1) z x) (tarai (- z 1) x y))))
これで実行してみると
debian:__pycache__$ time python tarai.cpython-36.pyc real 0m5.170s user 0m5.052s sys 0m0.004s
2秒も速くなった。でも、そのためにインライン化するの面倒。そういう事を Lispに携わる人が言ってはいけません。得意のマクロが有るじゃないですか。
benchstat
前回の続きで、benchmarkの事について調べていたんだ。そしたら、 How do I measure performance of elisp code?
なんてのがヒットした。profileerが使えるのね。 Emacs-24.3からネイティブプロファイラが使えてた 更に 注目は、最後のお勧めのところ。なにやらかっこいい 体裁で結果が表示される。
で、データを収集して、その結果を統計的にまとめるには、goのアプリを使っ てる。
(require 'benchstat) (defconst repetitions 10) (benchstat-run :old repetitions (xx 1)) (benchstat-run :new repetitions (xxNew 1)) (benchstat-compare)
以前に取り上げた、ひたすら計算するベンチコードxxとそれをbyte-compileし た、xxNewを俎上に載せてみた。
name old time/op new time/op delta Emacs 887ms ± 0% 652ms ± 4% -26.48% (p=0.000 n=8+9) name old allocs/op new allocs/op delta Emacs 318 ± 0% 318 ± 0% ~ (all equal)
演算時間は、26%高速化したけど、メモリーの使いっぷりは、一緒って結論に なりました。
(/ (- 887 652) 887.0) 0.2649379932356257
ielm
今までemacsで実験コードを試すには、*scrache* を使っていた。でも、考え るに、lispなんだから、正統派のreplが有ってもよさそう。調べたら出てきた。
(ac-config-default) (add-hook 'ielm-mode-hook 'ac-emacs-lisp-mode-setup) (add-to-list 'ac-modes 'inferior-emacs-lisp-mode)
こんな設定をしておくと、補完できるぞ(少々うざいけど)。 C-c C-bすると、バッファーを切りかえられる。
M-x ielm for elisp-repl
*** Welcome to IELM *** Type (describe-mode) for help.
ELISP> (defun ss(k)
(let ((n (* k 1000))
(s ""))
(dotimes (i n)
(setq s (concat s "Hello ELISP")))))
ss
ELISP> (benchmark-run (ss 4))
(1.072022584 61 0.951640164)
ELISP> (byte-compile 'ss)
#[(k)
"^H\305_\306^Y\211^Z\307^[^\^K\fW\205^[^@ \310P^Q^KT\211^S\202^K^@,\207"
[k s n i --dotimes-limit-- 1000 "" 0 "Hello ELISP"]
3]
ELISP> (benchmark-run (ss 4))
(0.370674901 15 0.23920097399999984)
前回取り忘れていた、文字列連結ベンチマーク。@ CentOS
ELISP> (/ 0.951640164 61) 0.015600658426229508 ELISP> (/ 0.23920097399999984 15) 0.01594673159999999 ELISP> (- 1.072022584 0.951640164) 0.12038241999999999 ELISP> (- 0.370674901 0.23920097399999984) 0.13147392700000016
最初の2つの割り算は、1回にかかるGCの時間。bytecodeに関係ない。次の2つ の引き算は、GC時間を除いた、演算時間。これは差がない。よって、GCの回数 減少で、スピードアップの効果が出ているんだな。
ってな事が、replが動いていると、簡単に確認できる。
load
定義した関数の置き場所は、どうなってるか調べてみた。
ELISP> (load-file "tarai.el")
t
ELISP> (symbol-function 'tarai)
(lambda
(x y z)
(if
(<= x y)
y
(tarai
(tarai
(1- x)
y z)
(tarai
(1- y)
z x)
(tarai
(1- z)
x y))))
ELISP> (load-file "tarai.elc")
t
ELISP> (symbol-function 'tarai)
#[(x y z)
"^H X\203^H^@ \207\303\211^HS \n#\303 S\n^H#\303\nS^H ##\207"
[x y z tarai]
7]
コンパイル前だと、文字列のまま格納されてる。コンパイルされたコードも、 同じ場所に入るんだな。
ELISP> (setq tarai "hogefuga") "hogefuga" ELISP> (symbol-value 'tarai) "hogefuga" ELISP> (tarai 12 6 0) 12 (#o14, #xc, ?\C-l)
関数と同名の変数を設定しても、関数が壊される事は無い。ここがschemeと違 う所だ。
dis and compile
ELISP> (disassemble
(byte-compile-sexp '(list (+ i 3) (* i 4))))
S式をコンパイルして、それを即座に逆アセンブルしちゃう慌しい例。
byte code: args: nil 0 varref i 1 constant 3 2 plus 3 varref i 4 constant 4 5 mult 6 list2 7 return
勿論コンパイル済みのコードは、何時でも逆アセンブル出来る。
ELISP> (disassemble 'tarai) byte code for tarai: args: (x y z) 0 varref x 1 varref y 2 leq 3 goto-if-nil 1 6 varref y 7 return 8:1 constant tarai 9 dup 10 varref x 11 sub1 12 varref y 13 varref z 14 call 3 :
この逆アセンブラ、ちょいとやり過ぎの感じがするぞ。xxはdotimesというマ クロを使っている。マクロは、展開して格納されてるんだろうなと思って、 確認したんだ。
ELISP> (symbol-function 'xx)
(lambda
(m)
(let
((n
(* m 1000 1000)))
(let
((--dotimes-limit-- n)
(i 0))
(while
(< i --dotimes-limit--)
(+
(+ i 0.1)
(- i 0.1)
(- i 0.2)
(+ i 0.2))
(setq i
(1+ i))))))
思った通り、展開形になってた。この状態で、逆アセすると、
byte code for xx: args: (m) 0 varref m 1 constant 1000000 2 mult 3 dup 4 varbind n 5 constant 0 6 varbind i 7 varbind --dotimes-limit-- 8 varref i 9:1 varref --dotimes-limit-- 10 lss 11 goto-if-nil-else-pop 2 14 constant + :
あたかも、コンパイル済みって形で表示される。人を惑わす仕様だな。騙され れて、時間詐欺にあわないように。
1000 * 1000 は、簡約と言うか、定数の畳み込みが行なわれているな。
disassemble is an interactive autoloaded compiled Lisp function in `disass.el'. (disassemble OBJECT &optional BUFFER INDENT INTERACTIVE-P) Print disassembled code for OBJECT in (optional) BUFFER. OBJECT can be a symbol defined as a function, or a function itself (a lambda expression or a compiled-function object). If OBJECT is not already compiled, we compile it, but do not redefine OBJECT if it is a symbol.
最後の2行が、邪魔者よ悩。
まてまて、これは89(hack)用の仕様じゃなかろうか。昔、あの方が、アレグロ から、正規表現の高速化を受けおった。最後は逆アセのコードとLispコードを 見比べながら、チューニングしたとか。そんな場面なら、コードをちょっと変 えて、マシンサイクルがどうなるか、爪に火を灯すような事をされたんだろう。
そんな時は、ありがたい鴨。amd64のサイクル数を把握されてる様な場合は、 開発のターンアラウンドが効率的になるんだろうね。
オイラーの場合は、 バイトコード リファレンス を見ながら、4989すればいいんだな。
;;; -*- lexical-binding: t -*-
とか言いながら、emacsには、魔法の1行が有るそうだ。上記のコメントを、 *.el の冒頭に書いておくだけって、お手軽な方法。いわゆる、プラグマだな。 こいつが書かれていると、高速モード用のbyte-codeを吐き出してくれるそう だ。なんじゃいな、レキシカルスコープって。
早速、魔法の効き具合を確認してみる。ss,xxは、魔法のコメント無しでコン パイルしたもの。頭にLが付いている方は、コメントで、lexical-bindingを宣 言した関数。
ELISP> (benchmark-run (ss 4)) (0.322944241 15 0.1950509459999994) ELISP> (benchmark-run (Lss 4)) (0.325183891 15 0.19779132499999985) ELISP> (benchmark-run (xx 1)) (0.604789484 31 0.41600492199999883) ELISP> (benchmark-run (Lxx 1)) (0.562540041 31 0.4165923730000003)
これを見ると、余り効果が感じられないな。ならば、taraiはどうよ?
ELISP> (benchmark-run (tarai 12 6 0)) (1.663158058 0 0.0) ELISP> (benchmark-run (Ltarai 12 6 0)) (0.90538161 0 0.0)
こちらは、効果絶大。実行時間が、元の54%までなった。魔法が上手く効くの と効かないのと、有るって事ね。何か体質に違いが有るのかしらん?
ELISP> (symbol-function 'ss)
#[(k)
"^H\305_\306^Y\211^Z\307^[^\^K\fW\205^[^@ \310P^Q^KT\211^S\202^K^@,\207"
[k s n i --dotimes-limit-- 1000 "" 0 "Hello ELISP"]
3]
ELISP> (symbol-function 'Lss)
#[257 "\211\300_\301^A\302\211^BW\205^Z^@\211^C\303P\262^D\210\211T\262^A\202^F\
^@\266\202\207"
[1000 "" 0 "Hello ELISP"]
8
("/tmp/Ltest.elc" . 590)]
変数リストから5個分が消えて、スタックに配置されたたっぽい。 本質的な部分で時間がかかっているんで(多分concat2が呼び出す、メモリー確 保)、工夫したのが相殺されちゃってるの かな?
逆アセしたのをpasteして並べておく(一部横幅に合わせて加工)。 6:1 とかの1は、逆アセが振ったラベル。
ss Lss
---------------------------- -------------------------------
0 varref k 0 dup
1 constant 1000 1 constant 1000
2 mult 2 mult
3 constant "" 3 constant ""
4 varbind s 4 stack-ref 1
5 dup 5 constant 0
6 varbind n 6:1 dup
7 constant 0 7 stack-ref 2
8 varbind i 8 lss
9 varbind --limit-- 9 goto-if-nil-else-pop 2
10 varref i 12 dup
11:1 varref --limit-- 13 stack-ref 3
12 lss 14 constant "Hello ELISP"
13 goto-if-nil-else-pop 2 15 concat2
16 varref s 16 stack-set 4
17 constant "Hello ELISP" 18 discard
18 concat2 19 dup
19 varset s 20 add1
20 varref i 21 stack-set 1
21 add1 23 goto 1
22 dup 26:2 discardN-preserve-tos 2
23 varset i 28 return
24 goto 1
27:2 unbind 4
28 return
varrefとかが、レキシカルスコープの方では、stack-refに置き変えられてい るのが、特徴的だな。
tarai Ltarai
--------------------------- ----------------------------
0 varref x 0 stack-ref 2
1 varref y 1 stack-ref 2
2 leq 2 leq
3 goto-if-nil 1 3 goto-if-nil 1
6 varref y 6 stack-ref 1
7 return 7 return
8:1 constant tarai 8:1 constant Ltarai
9 dup 9 dup
10 varref x 10 stack-ref 4
11 sub1 11 sub1
12 varref y 12 stack-ref 4
13 varref z 13 stack-ref 4
14 call 3 14 call 3
15 constant tarai 15 constant Ltarai
16 varref y 16 stack-ref 4
17 sub1 17 sub1
18 varref z 18 stack-ref 4
19 varref x 19 stack-ref 7
20 call 3 21 call 3
21 constant tarai 22 constant Ltarai
22 varref z 23 stack-ref 4
23 sub1 24 sub1
24 varref x 25 stack-ref 7
25 varref y 27 stack-ref 7
26 call 3 29 call 3
27 call 3 30 call 3
28 return 31 return
stack-refに置き変わっているのは、Lssと同じ。一瞬、stack-refのオペラン ドが間違っているんでは、と思うけど、PC相対なアドレッシングなの で、これで正しいはず(ですよね?)。
速度差が生まれるのは、方やvarrefでのアクセスでコストがかかり、レキシカ ルな方は、スタックへのアクセスだけで済むから? 塵も積もれば山となるっ てのの典型例かな?
stack-ref 7 は、2Byteのオペランドになってるね。
所で、taraiは何回呼ばれている? ちょいとカウンターを仕込んで計数してみ る。
;;; -*- lexical-binding: t -*-
(defun tarai (x y z)
(setq cnt (1+ cnt))
(if (<= x y)
y
(tarai (tarai (1- x) y z) (tarai (1- y) z x) (tarai (1- z) x y))))
(defun ct (x y z)
(let ((cnt 0))
(tarai x y z)
cnt))
上記でコンパイルすると、余計なものは混ぜちゃダメって怒られる。 レキシカルバインディングの弊害?かな。
Compiling file /tmp/aa.el at Wed Jan 16 15:43:49 2019 In tarai: aa.el:4:9:Warning: reference to free variable ‘cnt’ aa.el:4:17:Warning: assignment to free variable ‘cnt’
でも警告だからと思って、実行すると、
ELISP> (ct 12 6 0) *** Eval error *** Symbol’s value as variable is void: cnt
この通り、実行させてもらえなかった。警告は無視するなって典型ですな。 しょうがない ので、コメントを(コメント化は出来ない)外して、再コンパイルしたよ。 (コメントと言うかプラグマを無効にするには、2行目以降に移せば良い。)
ELISP> (ct 12 6 0) 12604861 (#o60052675, #xc055bd)
ふーむ、1260万回も呼び出されているよ。正に塵が積るんだろうね。