2011年3月14日月曜日

Emacsでadviceにより再帰呼び出し回数を調べる

Emacsの*scratch*バッファで、何らかの再帰的関数を書いたとする。適当なテストデータをその関数に与えて、実際に何回呼び出されるかを知りたい場合にどうするか、という話。

1. 組み込みの trace-function を使う

Emacsにはtrace-functionというコマンドが用意されており、関数のトレースというかコールグラフのようなものを表示できる。以下の例に示すように、実引数と戻り値も表示される。

;;; 再帰的関数の例(フィボナッチ数を「素朴に」求める)
(defun my-fib (n)
  "Return the nth fibonacci number."
  (if (<= n 2)
      1
    (+ (my-fib (- n 1)) (my-fib (- n 2)))))
=> my-fib

;;上で定義した再帰的関数に trace-fucntion を適用
(trace-function 'my-fib)
=> my-fib

;;; テストしてみる
(my-fib 4)   ;; 4番目のフィボナッチ数
=> 3

;;; trace-function の結果(*trace-output* というバッファに出力される)
1 -> my-fib: n=4      ;; 実引数 n=4
| 2 -> my-fib: n=3
| | 3 -> my-fib: n=2
| | 3 <- my-fib: 1    ;; 戻り値 1
| | 3 -> my-fib: n=1
| | 3 <- my-fib: 1
| 2 <- my-fib: 2
| 2 -> my-fib: n=2
| 2 <- my-fib: 1
1 <- my-fib: 3

このグラフを見ても一瞬では把握できないが、たとえばリージョンを選択して M-x count-matches -> my-fib を実行すると呼出し回数 = 5 だと分かる。

2. advice(アドヴァイス、アドバイス)を利用する

Emacsの"advice"という機能を利用すると、もっと直接的に目的を達成できる。

adviceとは既存の関数定義を変更することなしに独自の処理を追加する機能で、「アスペクト指向」と関連づけて紹介されることもあるためEmacsと縁がない分野にもadviceという用語を知っている人はいる。

なお、adviceに関連する概念は"pieces of advice"や"class", "name", "position", "flag"などたくさんあるので、ここでは省略(Elisp の info の "17 Advising Emacs Lisp Functions" に書いてある)。

概要

  • (my-call-count my-fib 4) の形で実行すると、my-fibの呼び出し回数である 5 が得られるようにする。
  • my-call-count は既存の関数 my-fib に呼び出し回数を数えるadviceを追加し、(my-fib 4) を実行し、数えた結果を返す。
  • 環境や既存の関数に影響が無いようにする(advice は使った後に消す、トップレベルの変数を使わない、シンボルを衝突させない、など)

my-call-countのコードと実行例

;;; 関数を実行して、呼び出し回数を表示するマクロ
(defmacro my-call-count (proc &rest args)
  (let ((ad-name (gensym))) ;任意のシンボルを作っておく(piece of advice の名前にする)
    `(let ((*my-call-cnt* 0))           ;カウンタ用の変数。もし重複してもシャドウされる。
       (defadvice ,proc   ;再帰的関数(proc)に piece of advice を追加
         (before ,ad-name first activate) ;最初から有効な"before-advice"とする
         "increment function call counter: *my-call-cnt*." ;これはコメント
         (incf *my-call-cnt*))                             ;インクリメントするだけ
       (,proc ,@args)                   ;piece of adviceを追加したので、再帰的関数を実行
       (ad-remove-advice ',proc 'before ',ad-name) ;実行が終わったので、追加した piece of advice を削除
       (ad-update ',proc)        ;削除したので関数をコンパイルし直す
       *my-call-cnt*)))
=> *my-call-cnt*

;;; マクロ展開形の確認(gensymの値、カンマ、カンマアットのあたりをチェック)
(macroexpand '(my-call-count my-fib 4))
=> (let ((*my-call-cnt* 0))
  (defadvice my-fib
    (before G48320 first activate)
    "increment function call counter: *my-call-cnt*."
    (incf *my-call-cnt*))
  (my-fib 4)
  (ad-remove-advice (quote my-fib) (quote before) (quote G48320))
  *my-call-cnt*)

;;; 実行してみる
(my-call-count my-fib 4)
=> 5           ;OK

;;; マクロ内で追加した piece-of-advice が削除されているかも確認
(ad-is-advised 'my-fib)
=> nil         ;ad-is-adviced の結果が nil ならOK

;;; しつこいけれど、変数名が重複しても大丈夫か確認
(defvar *my-call-cnt* 10)    ;トップレベルで変数に10をセット
=> *my-call-cnt*
(my-call-count my-fib 4)     ;マクロ実行
=> 5
*my-call-cnt*                ;トップレベルで評価し直してみる
=> 10                        ;10のままなのでOK
  

0 件のコメント:

コメントを投稿