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万回も呼び出されているよ。正に塵が積るんだろうね。