bytecode (2)
『蜂と蟻に刺されてみた』なんて本を借りてきた。英題は、"The STING of THE EORLD"。黄色の表紙に黒の題字で、見るからに危険そうな本である。
著者はアメリカ在住で痛さを求めて世界を放浪中の、イグ・ノーベル賞受賞の 昆虫科学者。彼独自の痛さ指標なるものを考案してる。
痛さは、個人の感覚なので統一的な指標は設定しずらい。世の中には、痛さ計 なる物が有るそうだ。皮膚に電極をつけて、電圧をかけ、痛さを測る計器。 でも、昆虫に刺された場合の痛さは様々なので、なじまないそうな。
そんな訳で、痛さ指標なるものを、1から4の数値で表す事にしたそうな。ミツ バチに刺された時の痛さを総合的に2と決めたとか。ミツバチを基準にするの は、どこにも居るから。数値が大きい程痛い。
こんな方法でも、業界の(昆虫学者)人にアンケートした所、衆目の一致する所 となったらしい。イグ・ノーベル賞の面目を保っているな。
刺す昆虫はメスだけだそうな。毒針は卵管と兼用だからね。オスの*あれ*は、 この場合は役立たず。集団行動する種が、捕食者から卵や蛹を守るために、毒 針を使うとか。
勿論、いきなり毒針攻撃ではなく、危険色での警告、羽音とかでの威嚇、匂で の攻撃気力の減退(と、味方への支援要請)等を行なうそうな。 自然界の進歩の賜物ですなあ。
巻末に、それぞれの種の痛さ指標が載ってた。気になる世界最強の痛さを示す 昆虫は、アメリカ大陸在住のタランチェラホークと言う蜂。痛レポには、
目がくらむほど凄まじい電撃的な痛み。風呂にはいっている時に感電したみた いな痛さ。
と、あった。4の指標はもう一種いて、ウォーリアワプスと言う蜂。こちらは、
拷問以外の何物でもない。火山の溶岩の中に鎖でつながれているみたい。
とあった。食レポで、旨いとしか言えないレポーターは、失格者と言われるが、 痛レポ者も大変だな。文学的表現が求められるよ。なんと、83種の違った表現 がなされていたぞ。
ちなみに、一昨年に注目を浴びた、ヒアリの指標は、1との事だった。マズコ ミ騒ぎすぎだな。
この学者さん、上記の種を全部経験したって言うそうなんで、マツコ・デラッ クスに出てくる変態さんと同種だな。
trace
schemeだと必須な手続であるtraceがemacsにも有った。前回の最後にやった、 taraiの呼び出し回数を計測する奴に適用してみる。
/tmp $ (trace-function 'tarai) /tmp $ (ct 2 1 0) 9
画面が割れて、下記の様な結果が得られた。
====================================================================== 1 -> (tarai 2 1 0) | 2 -> (tarai 1 1 0) | 2 <- tarai: 1 | 2 -> (tarai 0 0 2) | 2 <- tarai: 0 | 2 -> (tarai -1 2 1) | 2 <- tarai: 2 | 2 -> (tarai 1 0 2) | | 3 -> (tarai 0 0 2) | | 3 <- tarai: 0 | | 3 -> (tarai -1 2 1) | | 3 <- tarai: 2 | | 3 -> (tarai 1 1 0) | | 3 <- tarai: 1 | | 3 -> (tarai 0 2 1) | | 3 <- tarai: 2 | 2 <- tarai: 2 1 <- tarai: 2
traceを解除するには、untrace-functionを使うのね。 これ、有るかなと思って探したのではなく、偶然見つけたもの。
Emacs Lisp で SICP に挑戦するさいの落とし穴
こちらも偶然見つけた、変態もとえ風変りな方。そこにLispが有るならSICPだ ろうと動くはずって事です。
emacsを使う主目的は、editorとしてではなく、誰でも簡単に使えるLisp環境 だからです。(きっぱり!)
comment
犬も歩けば棒に当たる式に見つけたもの。1行目にプラグマとなる物を書ける。 どんなのが有るか、emacsの提供物を確認していて見つけた次第。 提供物は。見た限りでは、レキシカルバインディグが有効になってた。
もしやと思って、有志の提供物も確認。
;;; -*- no-byte-compile: t -*- ;;; w3m.el --- an Emacs interface to w3m -*- coding: iso-2022-7bit; -*- ;; w3m-bug.el --- command to report emacs-w3m bugs -*- coding: euc-japan -*- ;;; tuareg.el --- OCaml mode for Emacs. -*- coding: utf-8 -*- ;;; haskell-mode.el --- A Haskell editing mode -*-coding: iso-8859-1;-*-
他にどんな奴が有るのだろう?
A magic autoload comment (often called an "autoload cookie") consists of `;;;###autoload',
これは知ってたけど、まとめみたいの何処かにないかな?
The byte compiler uses the dynamic function loading feature if the variable `byte-compile-dynamic' is non-`nil' at compilation time. Do not set this variable globally, since dynamic loading is desirable only for certain files. Instead, enable the feature for specific source files with file-local variable bindings. For example, you could do it by writing this text in the source file's first line: -*-byte-compile-dynamic: t;-*-
infoをfirst lineで(s)earchするぐらいしか無いのかな。難儀なこっちゃ。
電卓
前回やったtarai関数のダイナミック/レキシカルスコープの違いによる実行ス ピードの差を解析したい。その前段階として、ちょっと分析。電卓が必要にな るな。emacsに内蔵してるだろう。どうやら2種類有るみたい。
その一つ目は、M-x calculator で起動する。バインド方法が書いてあったけ ど、公式なのは無い。起動すると、
Calc=H=> ?
こんな風になるので、?を押してみた。
Quick reference: * numbers/operators/parens/./e - enter expressions + - * / \(div) %(rem) _(-X,postfix) ;(1/X,postfix) ^(exp) L(og) Q(sqrt) !(fact) S(in) C(os) T(an) |(or) #(xor) &(and) ~(not) * >/< repeats last binary operation with its 2nd (1st) arg as postfix op * I inverse the next trig function * '/"/{/} - display/display args * D - switch to all-decimal, or toggle deg/rad mode * B/O/H/X - binary/octal/hex mode for i/o (both H and X are for hex) * i/o - prefix for D/B/O/X - set only input/output modes * enter/= - evaluate current expr. * s/g - set/get a register * space - evaluate & save on list * l/v - list total/average * up/down/C-p/C-n - browse saved * C-delete - clear all saved * C-insert - copy whole expr. * C-return - evaluate, copy, exit * insert - paste a number * backspace- delete backwards * delete - clear argument or list value or whole expression (twice) * escape/q - exit. [back]
使い方は、普通っぽい。1 + 2 の計算をするなら、1 SPC + SPC 2 SPC とする。 SPCの変りにRETでもよい。
もう一種は、C-x * c すれば、とりあえず RPN電卓が使えるようになる。こやつ、左側が入力用、右側が履歴表示窓になっ ている。 高級なんで、説明書があった。ざっと見、gnuplotと連携してグラフも書ける みたいだ。
The GNU Emacs Calculator(ja) Calc 2.02
慣れているなら、こちらの方が使い易いかな。なにせ安い(なんと無償ですよ)HP電卓ですからね。
--- Emacs Calculator Mode --- | 1.663158058 1: 6.01177948729e-8 | 0.90538161 . | - 0.757776448 | 12604861 | / 6.01177948729e-8 -=UU:%*--F1 Calc: 12 Deg All (3,4)|-=UU:%*--F1 *Calc Trail*
これ、前回やったtaraiの呼び出しの1回あたりの差。1.663がダイナミックス コープの実行時間、0.905がレキシカルの環境での実行時間。1260万回実行さ れてるんで、差をそれで割ってみた。60nsがその結果。
tarai関数は、途中でリターンしちゃう場合があるから一概には言えないけど、 平均で10回、varref(or stack-ref)が実行されてると仮定すると、bytecodeの 1回あたりの差は、6nsとなるな。予想した通り塵な時間。石の速度からしても、 妥当な値と思われる。
このRPN電卓を自在に扱える人は、bytecodeの実行機構をフムフムと理解でき る素養がありますよ。
run bytecode
ってな事で、bytecodeを走らせてみる。 batchで起動すると、emacsの図体が出てこないので、gdbと相性が良い。
cent:tmp$ gdb -q emacs Reading symbols from /usr/local/bin/emacs-26.1...done. (gdb) r --batch -l tarai.elc Starting program: /usr/local/bin/emacs --batch -l tarai.elc [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1".
適当な所で止めて見た。
(gdb) bt #0 SPECPDL_INDEX () at lisp.h:3130 #1 exec_byte_code at bytecode.c:1453 #2 0x00000000005037bc in funcall_lambda at eval.c:2970 #3 0x0000000000503a33 in Ffuncall at eval.c:2783 #4 0x0000000000537e8f in exec_byte_code at bytecode.c:629 #5 0x00000000005037bc in funcall_lambda at eval.c:2970 #6 0x0000000000503a33 in Ffuncall at eval.c:2783 #7 0x0000000000537e8f in exec_byte_code at bytecode.c:629 <snip> #242 0x0000000000537e8f in exec_byte_code at bytecode.c:629 #243 0x00000000005037bc in funcall_lambda at eval.c:2970 #244 0x0000000000503a33 in Ffuncall at eval.c:2783 #245 0x0000000000537e8f in exec_byte_code at bytecode.c:629 #246 0x00000000005037bc in funcall_lambda at eval.c:2970 #247 0x0000000000502b83 in apply_lambda at eval.c:2906 #248 0x0000000000502eb7 in eval_sub at eval.c:2309 #249 0x00000000005069f0 in Feval at eval.c:2054 #250 0x0000000000502164 in internal_condition_case at eval.c:1332 #251 0x00000000004931ac in top_level_1 at keyboard.c:1127 #252 0x0000000000502114 in internal_catch at eval.c:1097 #253 0x0000000000490cb6 in command_loop () at keyboard.c:1088 #254 0x000000000049516e in recursive_edit_1 () at keyboard.c:695 #255 0x0000000000495485 in Frecursive_edit () at keyboard.c:766 #256 0x0000000000409103 in main at emacs.c:1713
こちらは、最後に出てくる、lispベースのback-trace。loadが動いているのね。
: "tarai" (0xffffc8f0) "tarai" (0xffffca20) "load" (0xffffce98) "command-line-1" (0xffffd490) "command-line" (0xffffdd48) "normal-top-level" (0xffffe040)
まんまのbytecode.cを見る。 細かい塵を探すため。
最初にレキシカルスコープで良く使われる部分。すっきりしたコードになって て、速く動きそう。
/* Handy byte-codes for lexical binding. */ CASE (Bstack_ref1): CASE (Bstack_ref2): CASE (Bstack_ref3): CASE (Bstack_ref4): CASE (Bstack_ref5): { Lisp_Object v1 = top[Bstack_ref - op]; PUSH (v1); NEXT; } CASE (Bstack_ref6): { Lisp_Object v1 = top[- FETCH]; PUSH (v1); NEXT; } CASE (Bstack_ref7): { Lisp_Object v1 = top[- FETCH2]; PUSH (v1); NEXT; }
それに対して、こちらは従来のダイナミックスコープ用。
CASE (Bvarref7): op = FETCH2; goto varref; CASE (Bvarref): CASE (Bvarref1): CASE (Bvarref2): CASE (Bvarref3): CASE (Bvarref4): CASE (Bvarref5): op -= Bvarref; goto varref; /* This seems to be the most frequently executed byte-code among the Bvarref's, so avoid a goto here. */ CASE (Bvarref6): op = FETCH; varref: { Lisp_Object v1 = vectorp[op], v2; if (!SYMBOLP (v1) || XSYMBOL (v1)->u.s.redirect != SYMBOL_PLAINVAL || (v2 = SYMBOL_VAL (XSYMBOL (v1)), EQ (v2, Qunbound))) v2 = Fsymbol_value (v1); PUSH (v2); NEXT; }
gotoが出てきたり、判定が含まれていたりで、オーバーヘッドが大きいな。
ちょいと、いい加減な方法だけど、時間を喰っていそうなifの判定の所をステッ プ実行してみたら、9ステップかかっていた。amd64で、9マシンサイクルって 事になる。1マシンサイクルを1nsで実行したとして、無駄時間は9nsになる。 なんとなく、計算が合致するような(こじつけっぽい?)
勝手に1マシンサイクルを1nsなんて仮定したけど、その根拠は? えーとね、 L1キャッシュが、どうたらこうたらとかかな? Windowsならば、
丁度よい記事が出てた。リナなら、/proc/cpuinfo
model name : Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz stepping : 3 microcode : 0xc6 cpu MHz : 2400.003 cache size : 3072 KB
このあたりかな。
より道したな。ついでに、stack操作のマクロを確認しておく。
/* Fetch the next byte from the bytecode stream. */ #define FETCH (*pc++) /* Fetch two bytes from the bytecode stream and make a 16-bit number out of them. */ #define FETCH2 (op = FETCH, op + (FETCH << 8)) /* Push X onto the execution stack. The expression X should not contain TOP, to avoid competing side effects. */ #define PUSH(x) (*++top = (x)) /* Pop a value off the execution stack. */ #define POP (*top--) /* Discard n values from the execution stack. */ #define DISCARD(n) (top -= (n)) /* Get the value which is at the top of the execution stack, but don't pop it. */ #define TOP (*top)
bytecode.c/exec_byte_code関数と、eval.c/funcall_lambda関数の最後の部分 とのいったりきたりを繰り代えしてる。
if (CONSP (fun)) val = Fprogn (XCDR (XCDR (fun))); else { /* If we have not actually read the bytecode string and constants vector yet, fetch them from the file. */ if (CONSP (AREF (fun, COMPILED_BYTECODE))) Ffetch_bytecode (fun); => val = exec_byte_code (AREF (fun, COMPILED_BYTECODE), AREF (fun, COMPILED_CONSTANTS), AREF (fun, COMPILED_STACK_DEPTH), Qnil, 0, 0); } return unbind_to (count, val);
more bytecode
もう少しbytecodeを見ておく。基本的な奴
CASE (Bcons): { Lisp_Object v1 = POP; TOP = Fcons (TOP, v1); NEXT; }
これ、基本の基だな。説明書には、こんな記述があった。
cons (66) Call cons with two stack arguments. See Section “Building Cons Cells and Lists” in The GNU Emacs Lisp Reference Manual, for a description of this Emacs Lisp function. Implements: S[1] <- (cons S[1] TOS); top--. Generated via: cons. Instruction size: 1 byte Stack effect: −2 + 1. Example: (defun cons-eg() (cons ’a ’b)) generates: PC Byte Instruction 0 192 constant[0] a 1 193 constant[1] b 2 66 cons 3 135 return Constants Vector: [a b]
Stack effectなんて項に、スタックの消費状況が記述されてるのは、Forthの 由来。
もう一丁、大事な奴。
: CASE (Bcall4): CASE (Bcall5): op -= Bcall; docall: { DISCARD (op); TOP = Ffuncall (op + 1, &TOP); NEXT; }
DISCARDの後に、チェックコードが(コンパイルオプションで追加してあるけど) 省略して表示。
call(32–39) Calls a function. The instruction argument specifies the number of arguments to pass to the function from the stack, excluding the function itself. Implements: (set_internal(constants_vector[i]) where i is the value of the instruction operand. Instruction size: 1 byte for call[0] .. call[4] ; 2 bytes for call[5] , 8-bit operand; 3 bytes for call[6] , 16-bit operand. Stack effect: −0 + 1. Example: (defun call-eg() (exchange-point-and-mark) (next-line 2)) generates: PC Byte Instruction 0 192 constant[0] exchange-point-and-mark 1 32 call[0] 2 136 discard 3 193 constant[1] next-line 4 194 constant[2] 2 5 33 call[1] 6 135 return Constants Vector: [exchange-point-and-mark next-line 2]
よく出てくる命令はbytecodeに収録されてるけど、そうじゃない奴(ユーザー が定義した物も含む)は、定数テーブルに登録されて、それが使われるのだな。
callからの返値は、スタックに積まれるけど、それが不要ならdiscard命令で 破棄されるようにコンパイラがコード生成してくれるとな。
amd64 (and i386) asm
先の内容で、マシンの実行命令数を数えるのに、何ステップをgdbで実行した か、数えた。負けた気分。そんな時は、gdbでlayout src/asm とかして、生ア センプラコードを出すのが定石。
でも、CentOSのそれでは、画面が崩れるし、根源的にどのあたりを回っている か、わからない。
そこで、
cent:src$ objdump -d -S bytecode.o : varref: { Lisp_Object v1 = vectorp[op], v2; 237: 48 8b 4d c0 mov -0x40(%rbp),%rcx 23b: 48 98 cltq 23d: 48 8b 3c c1 mov (%rcx,%rax,8),%rdi if (!SYMBOLP (v1) 241: 40 f6 c7 07 test $0x7,%dil 245: 75 12 jne 259 <exec_byte_code+0x239> || XSYMBOL (v1)->u.s.redirect != SYMBOL_PLAINVAL 247: 0f b6 87 00 00 00 00 movzbl 0x0(%rdi),%eax 24e: 83 e0 0e and $0xe,%eax 251: 3c 08 cmp $0x8,%al 253: 0f 84 74 1f 00 00 je 21cd <exec_byte_code+0x21ad> || (v2 = SYMBOL_VAL (XSYMBOL (v1)), EQ (v2, Qunbound))) v2 = Fsymbol_value (v1); 259: e8 00 00 00 00 callq 25e <exec_byte_code+0x23e> PUSH (v2); 25e: 48 89 43 08 mov %rax,0x8(%rbx) NEXT; 262: 41 0f b6 34 24 movzbl (%r12),%esi 267: 49 8d 44 24 01 lea 0x1(%r12),%rax PUSH (v2); 26c: 48 83 c3 08 add $0x8,%rbx NEXT;
emacsをコンパイルした時の残骸オブジェクトとCのソースを重ね合わせてみた。
こちらは、i386なdebianで、layout asm した時の図。varrefのifの近辺。
+---------------------------------------------------------------------------+ |0x8181db3 <exec_byte_code+643> mov -0x40(%ebp),%esi | |0x8181db6 <exec_byte_code+646> mov (%esi,%eax,4),%edx | >|0x8181db9 <exec_byte_code+649> test $0x7,%dl | |0x8181dbc <exec_byte_code+652> jne 0x8181dd7 <exec_byte_code+679> | |0x8181dbe <exec_byte_code+654> mov -0x1c(%ebp),%eax | |0x8181dc1 <exec_byte_code+657> mov %edx,%ecx | |0x8181dc3 <exec_byte_code+659> add $0x846d8a0,%ecx | |0x8181dc9 <exec_byte_code+665> movzbl (%ecx),%eax | |0x8181dcc <exec_byte_code+668> and $0xe,%eax | |0x8181dcf <exec_byte_code+671> cmp $0x8,%al | |0x8181dd1 <exec_byte_code+673> je 0x818204a <exec_byte_code+1306> | |0x8181dd7 <exec_byte_code+679> sub $0xc,%esp | |0x8181dda <exec_byte_code+682> mov -0x1c(%ebp),%ebx | |0x8181ddd <exec_byte_code+685> push %edx | |0x8181dde <exec_byte_code+686> call 0x8138b10 <Fsymbol_value> | |0x8181de3 <exec_byte_code+691> add $0x10,%esp | |0x8181de6 <exec_byte_code+694> addl $0x4,-0x20(%ebp) | |0x8181dea <exec_byte_code+698> mov -0x1c(%ebp),%esi | |0x8181ded <exec_byte_code+701> mov -0x20(%ebp),%ecx | |0x8181df0 <exec_byte_code+704> mov %eax,(%ecx) | |0x8181df2 <exec_byte_code+706> mov %edi,%eax | |0x8181df4 <exec_byte_code+708> add $0x1,%edi | +---------------------------------------------------------------------------+
よう分からんので、ソースと重ねてみた。
2a0: 83 e8 08 sub $0x8,%eax Lisp_Object v1 = vectorp[op], v2; 2a3: 8b 75 c0 mov -0x40(%ebp),%esi 2a6: 8b 14 86 mov (%esi,%eax,4),%edx if (!SYMBOLP (v1) 2a9: f6 c2 07 test $0x7,%dl 2ac: 75 19 jne 2c7 <exec_byte_code+0x2a7> return lisp_h_XSYMBOL (a); 2ae: 8b 45 e4 mov -0x1c(%ebp),%eax 2b1: 89 d1 mov %edx,%ecx 2b3: 03 88 00 00 00 00 add 0x0(%eax),%ecx || XSYMBOL (v1)->u.s.redirect != SYMBOL_PLAINVAL 2b9: 0f b6 01 movzbl (%ecx),%eax 2bc: 83 e0 0e and $0xe,%eax 2bf: 3c 08 cmp $0x8,%al 2c1: 0f 84 73 02 00 00 je 53a <exec_byte_code+0x51a> v2 = Fsymbol_value (v1); 2c7: 83 ec 0c sub $0xc,%esp 2ca: 8b 5d e4 mov -0x1c(%ebp),%ebx 2cd: 52 push %edx 2ce: e8 fc ff ff ff call 2cf <exec_byte_code+0x2af> 2d3: 83 c4 10 add $0x10,%esp PUSH (v2); 2d6: 83 45 e0 04 addl $0x4,-0x20(%ebp) NEXT; 2da: 8b 75 e4 mov -0x1c(%ebp),%esi PUSH (v2); 2dd: 8b 4d e0 mov -0x20(%ebp),%ecx 2e0: 89 01 mov %eax,(%ecx) NEXT;
こちらは、i386な奴です。なんとなく、かわゆい気持になるな。