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と連携してグラフも書ける みたいだ。

Calc

The GNU Emacs Calculator(ja) Calc 2.02

The GNU Emacs Calculator(en)

慣れているなら、こちらの方が使い易いかな。なにせ安い(なんと無償ですよ)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な奴です。なんとなく、かわゆい気持になるな。