ecl (5)
『匠の技 歌舞伎座をつくる』なんて本を読んでみた。この間から読書メータを付けて いるので、この本の価格は? と調べてみたんだけど、何処にも書いてない。 それもそのはず、清水建設が全国の公立図書館へ寄贈するために作成したものだから。
歌舞伎っていうと、昔女房がスーパー歌舞伎とやらを良く観劇に行っていた。おいらはと 言うと、去年だか、銀座に出た時に入り口のみやげ物を売ってる広場を見学して、回りを ぐるっと回ったぐらい。意匠を凝らした建物にびっくりした次第。
今回で、5代目の建物になるとか。初代は明治22年(1889年)に竣工。関東大震災とか 大空襲とかで破壊されても、歌舞伎を愛する松竹とか清水建設とか色々な方面の支援で 復興してきたらしい。
元々、清水建設は神社仏閣の建設が得意な会社。今でも木を専門に扱う木工所を持って いて、昔からの技術を伝承してるとか。勿論、歌舞伎座独特の意匠は木だけでは成り立たない。 瓦や舞台とかは、専門の技術集団が居るわけだけど。
建て替えに当たって、戦後60年間愛された歌舞伎座を継承する事が大きなテーマになった とか。継承するだけでなく、そこに新しい技術を注入してく。この難しい事業を各方面の 協力を得ながらやってく様は、まさに匠の技ですな。古くて新しいもの、何かLispとかに 似ているぞ。
歌舞伎座と言えば、歌舞伎座タワーも有名ですな。旧と新のコラボで、銀座の狭い土地を 有効利用しましょって訳で、タワービルも建てちゃった。これで、歌舞伎が廃っても 、大家が困る事は余りなかろう。道楽の歌舞伎がずっと続けられれば万万歳。 何と言っても、歌舞伎は世界で一番に金を喰う演劇らしいです。今の所は、歌舞伎の人気に 支えられて、黒字経営らしいです。
歌舞伎タワーの店子で有名なのは、あのニコ動で有名なドワンゴ。つてをつたって 見学に行ってきた人が、大量に写真を上げていたので、ごゆるりとご覧あれ。
今回の読書メータ 6冊 / 1558頁 / 9000円 + a
ansicl-chm
例によって、profile.lispのコードをちらちらと見てる訳だけど、おいらの知らない関数が 頻出してる。ならば、slime上から、知らない関数にポインターを合わせて C-c C-d h すれば 済む話では。それが正しい道とは存じてますが、なんかそう、もっと俯瞰したい事も有るんよ。
で、探してきたのが、こちら。 ANSI Common Lisp の仕様書を HTML Help にした Windows専用でございます。Linuxを立てなくてもご覧頂けます。たまには、こういうのもいい鴨。 でも、例とか欲しいぞ。そんな場合は、 The Common Lisp Cookbookが有りますがな。 日本語で書かれたやつは? 逆引きCommon Lispでございます。
そして、使いでのあるformatも、 Common Lispのformat関数 で説明されてます。あれ?もう一つ大事なやつがあるよね。loop言語が。
はい、実践CLでも、黒帯のためのLoopとか言って、脅かしてますね。でもね、これって、 規格になっちゃってるんで、無闇やたらに変更出来んのよ。今は、その規格をすりぬける 企画が進行中さ。なんでも、イテレータとか言うらしい。やっと、世間並みの用語が 出てきたなあ。これもそれもCLの養護のためですって。
なお、どうしてもloopって場合には、 Common Lisp: loopマクロ用法抄 が、便利です。
ecl profile
が、Bugっぽいって事で、コードを紐解いてみた。核心はどうもここっぽい。
(let ((dticks 0) : (start-ticks (get-internal-ticks)) (start-consed (get-bytes-consed 0))) (unwind-protect (progn (setf *enclosed-ticks* 0 *enclosed-profiles* 0 *enclosed-consing* 0) (apply encapsulated-fun args)) (setf dticks (- (get-internal-ticks) start-ticks)) (print dticks) (setf dconsing (get-bytes-consed start-consed)) (setf inner-enclosed-profiles *enclosed-profiles*)
printって関数は、オイラーがdebugの為に入れた痛撃の一行だ。unwindって、第一関数が 若し失敗しても、第二関数以降をさらっと実行するっていう保護機構だ。この場合の 第一関数って、prognね。この中で、profileを実行したい関数をapplyしてる。
実行の前後で、メータを読んでる。そして、メータの差、すなわち実行時間をdticksにbind してる。メータを読む関数は、get-internal-ticksってなってるけど、これは前の方で get-internal-run-timeのエイリアスとして定義してる。これって、移植性の確保だね。
ともかく、これで、1ms分解能で、実行時間を測定出来るしかけだ。と、言う事は、実行時間が 1ms以内だと、メータが変化しない。
takなんかは、実行すると直ぐにreturnしてしまう。本当かどうか、sbclで確認してみた。 (sbclの方が、綺麗な結果を残してくれたので、使ったまでだ。)
* (trace tak) (TAK) * (tak 2 1 0) 0: (TAK 2 1 0) 1: (TAK 1 1 0) 1: TAK returned 0 1: (TAK 0 0 2) 1: TAK returned 2 1: (TAK -1 2 1) 1: TAK returned 1 1: (TAK 0 2 1) 1: TAK returned 1 0: TAK returned 1 1
再帰を使ってコードが書かれているんだけど、呼び出しが深くならないように工夫され てて、これは関数呼び出しのオーバーヘッドを計測するんだな。関数内の計算と言えば、 大小比較と、整数の引き算が有るだけだから、たいした事ではない。
上記を確かめる為、呼び出し回数が明確になってるfactを使って実験してみる。 但しfactも、呼び出すと直ぐに戻ってしまうので、内部で無駄時間を過ごすように してみた。
(defun lg () (dotimes (i 1000000) (1+ i))) (defun fact (n) (dotimes (i 1000000) (1+ i)) (if (zerop n) 1 (* n (fact (- n 1)))))
debug printが有効になっているので、舞台裏が見える。
248 240 468 700 936 1164 seconds | consed | calls | sec/call | name ---------------------------------------------------- 1.164 | 0 | 5 | 0.232800 | FACT 0.248 | 0 | 1 | 0.248000 | LG ---------------------------------------------------- 1.412 | 0 | 6 | | Total
どうやら使い方のBugが有ったようだ。疑って御免。馬鹿と鋏は使い様と言うけど、簡単な やつは、timeマクロを使うのが正解だな。
> (time (fact 4)) real time : 1.152 secs run time : 1.148 secs gc count : 1 times consed : 18165808 bytes 24
consedは、収集のポリシーが違うので、結果が異なっているとな。これで、悩み解消 ですかね。
まてまて、折角なんで、キャリブレーションがどうなってるか、見ておくか。
(defparameter *timer-overhead-iterations* 500000) (defun compute-overhead-aux (x) (declare (ignore x))) (defun compute-overhead () (format *debug-io* "~&Measuring PROFILE overhead..") (flet ((frob () (let ((start (get-internal-ticks)) (fun (symbol-function 'compute-overhead-aux))) (declare (type function fun)) (dotimes (i *timer-overhead-iterations*) (funcall fun fun)) (/ (float (- (get-internal-ticks) start)) (float +ticks-per-second+) (float *timer-overhead-iterations*))))) :
人畜無害な試供関数を定義。それを500000回実行するような関数、frobってのを定義。 CLはfletとかlabelsみたいに微妙に働きが異なるものを用意してるんで、面食らうぞ。 こちらの方も悩み多きかなですな。 labelsとfletの違い。 schemeだと、何でもdefineで通しちゃうから脳内負荷が下がりますよ。 そう言えば、CLでdef何とかってのが多すぎますよ。全く困ったものだ。 歳を取ったら、schemeに限りますな。
まあ、折角なんで、補正値を取得してみるか。
> (mine::compute-overhead) Measuring PROFILE overhead..done #S(MINE::OVERHEAD :MINE::CALL 0.0 :MINE::INTERNAL 9.84e-7 :MINE::TOTAL 2.016e-6)
compute-overheadは、exportされていないんで、普通は使う事が出来ないんだけど、 パッケージ名の後にコロンを2つ重ねると、その紳士協定を打ち破る事が出来る。 AnsiCLでは、紳士協定を破るなんて、身の振り方を見直せって戒めていますんで、 あくまで、こっそり使いましょう。
所で、上に出てきた#Sって何よ? ansicl_chmで調べてみると、2.4.8.13って所に有った。
#s(name slot1 value1 slot2 value2 ...) denotes a structure. This is valid only if name is the name of a structure type already defined by defstruct and if the structure type has a standard constructor function. Let cm stand for the name of this constructor function; .....
シンタックス(2)のうちのマクロ文字(2.4)のうちの頭に#が付く2.4.8)文字になるのね。
hashとやら
どうやらCLにもhashが有るようで、profileの中でも使われていた。Lispの伝統の属性Listは もう時代遅れなんでしょうかね。
逆引きLispから、hashの内容をダンプする例を引いてきました。名前はmaphashですって。 基本の基のkeyを与えて値を引いてくるのは、gethash。分かり易い。こうでなきゃ、Lisp 恐いとなってしまいますからね。
> (maphash (lambda (key val) (format t "key => ~A, val => ~A~%" key val)) mine::*profiled-fun-name->info*) key => FACT, val => #S(MINE::PROFILE-INFO :MINE::NAME FACT :MINE::ENCAPSULATED-FUN #<bytecompiled-function FACT> :MINE::ENCAPSULATION-FUN #<compiled-closure 09cf54c8> :MINE::READ-STATS-FUN #<compiled-closure 09cf54b0> :MINE::CLEAR-STATS-FUN #<compiled-closure 09cf5498>) key => TAK, val => #S(MINE::PROFILE-INFO :MINE::NAME TAK :MINE::ENCAPSULATED-FUN #<bytecompiled-function TAK> :MINE::ENCAPSULATION-FUN #<compiled-closure 09cf5468> :MINE::READ-STATS-FUN #<compiled-closure 09cf5450> :MINE::CLEAR-STATS-FUN #<compiled-closure 09cf5438>) key => LG, val => #S(MINE::PROFILE-INFO :MINE::NAME LG :MINE::ENCAPSULATED-FUN #<bytecompiled-function LG> :MINE::ENCAPSULATION-FUN #<compiled-closure 09cf5408> :MINE::READ-STATS-FUN #<compiled-closure 09cf53f0> :MINE::CLEAR-STATS-FUN #<compiled-closure 09cf53d8>)
確かに、3つの関数を登録しましたよ。基本的には、
> mine::*profiled-fun-name->info* #<hash-table 09ce99c0> > (gethash 'fact mine::*profiled-fun-name->info*) #S(MINE::PROFILE-INFO :MINE::NAME FACT :MINE::ENCAPSULATED-FUN #<bytecompiled-function FACT> :MINE::ENCAPSULATION-FUN #<compiled-closure 09cf54c8> :MINE::READ-STATS-FUN #<compiled-closure 09cf54b0> :MINE::CLEAR-STATS-FUN #<compiled-closure 09cf5498>) T
ちょいと見やすいように整形した。hashの値も構造化してあって、profile対象の名前、対象の 関数名、計測値登録関数、結果呼び出し関数、結果のクリア関数が登録されてる。関数を登録 しちゃうってのが、Lisp屋さんの頭の中を覗いているようで、実に面白い。
> (setq pinfo (gethash 'fact mine::*profiled-fun-name->info*)) > (funcall (mine::profile-info-read-stats-fun pinfo)) 5 1172 0 10 > (mine::report) seconds | consed | calls | sec/call | name ---------------------------------------------------- 1.172 | 0 | 5 | 0.234398 | FACT :
hashからfactの値を取り出しpinfoにセット。その中から、呼び出し関数を取り出して、 funcallで、生の値を取り出してみた。これを整形してくれるのが、レポートの役目だな。
生の値が見える所に格納されていなくて、呼び出し関数を経由しなければ取り出せ無い って設計、何かにかぶれているようにしか思えんな。ああ、クロージャ使いたかったんか。 あれは、奥が深いですからね。素人お断りって臭いがプンプンするよ。
そうそう、profile-info-read-stats-funを実行した時の返値が、パラパラと表示されている。 こういうのは、なかなかお目にかからないぞ。コードがどうなってるか見たら、面白い事に 気付いた。
CL-USER> (values 1 2 3) 1 2 3
多値を返すってやつね。どんな風に使うかって、余り良い例が浮かばないけど。
CL-USER> (defun fuku (n) (values (sin n) (cos n) (tan n) n)) FUKU CL-USER> (fuku (/ pi 3)) 0.86602540378443864673l0 0.5l0 1.7320508075688772934l0 1.0471975511965977461l0
sin,cos,tanとその元入力値を一遍に得る事が出来る。
CL-USER> (setq aa (fuku (/ pi 4))) 0.70710678118654752444l0 CL-USER> aa 0.70710678118654752444l0
普通に(値を一つしか要求しない関数)使うと、一番目の結果が利用される。後は棄てられる。 全部を使用したい場合は、multiple-value-bind で、受けてやれば良い。
CL-USER> (multiple-value-bind (s c t n) (fuku (/ pi 4)) (list s c t n)) (0.70710678118654752444l0 0.7071067811865475244l0 1.0l0 0.78539816339744830963l0)
sbclはどうよ
eclのprofileは大体分かった。そんじゃsbclはどうよ? 気分転換に河岸を変えてみる。
やっぱりsbclのソースが有った方が良いな。ソオスを取り寄せたのはいいんだけど、INSTALL なんて文書に釣られて、思わずコンパイルで16分の無駄を喰っちゃったぞ。 何でも、ソースからコンパイルするには、sbclとかcmuclとかの環境が必要との事。
コンパイル時のログを眺めてみたけど、gccが出てくるのはほんの冒頭だけ。後はひたすら lisp語のコンパイルに費やされていましたよ。sbclはlispで出来ているんだなあと、強く 感じました。
ああ、本題に戻って、sbclのprofileですな。マニュアルをしっかり読むと、2種類の profileが利用出来るとあります。一つは、eclにも採用されてたCMUCL由来のやつ。 もう一つは、サンプリング方式によるやつ。以前マニュアルを見た時、早とちりしちゃった やつです。
どんな具合に動くかな。
* (require :sb-sprof) ("SB-SPROF") * (sb-sprof:with-profiling (:max-samples 1000 :report :flat :loop t) (tak 20 10 0)) Profiler sample vector full (263 traces / 10000 samples), doubling the size Profiler sample vector full (527 traces / 20000 samples), doubling the size Number of samples: 1000 Sample interval: 0.01 seconds Total sampling time: 10.0 seconds Number of cycles: 0 Sampled threads: #<SB-THREAD:THREAD "main thread" RUNNING {ABFC9F9}> Self Total Cumul Nr Count % Count % Count % Calls Function ------------------------------------------------------------------------ 1 780 78.0 1000 100.0 780 78.0 - TAK 2 220 22.0 220 22.0 1000 100.0 - SB-VM::GENERIC-+ 3 0 0.0 1000 100.0 1000 100.0 - "Unknown component: #xB0FDA08" 4 0 0.0 1000 100.0 1000 100.0 - SB-INT:SIMPLE-EVAL-IN-LEXENV 5 0 0.0 1000 100.0 1000 100.0 - EVAL 6 0 0.0 1000 100.0 1000 100.0 - INTERACTIVE-EVAL 7 0 0.0 1000 100.0 1000 100.0 - SB-IMPL::REPL-FUN 8 0 0.0 1000 100.0 1000 100.0 - (LAMBDA NIL :IN SB-IMPL::TOPLEVEL-REPL) 9 0 0.0 1000 100.0 1000 100.0 - SB-IMPL::%WITH-REBOUND-IO-SYNTAX 10 0 0.0 1000 100.0 1000 100.0 - SB-IMPL::TOPLEVEL-REPL 11 0 0.0 1000 100.0 1000 100.0 - SB-IMPL::TOPLEVEL-INIT 12 0 0.0 1000 100.0 1000 100.0 - (FLET #:WITHOUT-INTERRUPTS-BODY-44 :IN SAVE-LISP-AND-DIE) 13 0 0.0 1000 100.0 1000 100.0 - (LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE) ------------------------------------------------------------------------ 0 0.0 elsewhere #<SB-SPROF::CALL-GRAPH 1000 samples {BC61851}>
これって、10ms毎にサンプリングして、どこを実行してたかの比率を求める方式なのね。 よく、実行時間の80%はコードの20%に集中してるって言うから、ホットスポットの検出に うってつけなんだな。
早速コード探求。こんな所に有ったよ。
sakae@uB:~/sbcl-1.2.0/contrib/sb-sprof$ wc sb-sprof.lisp 1416 6022 60146 sb-sprof.lisp
誰かさんから贈呈されたやつだな。
thread
早速見て行くと、threadなんてwordが飛び交っているぞ。そうか、一定時間毎に サンプリングをするなら、タイマーが必須だものな。
一つは、目的関数を実行するスレッド。もう一つは、一定時間毎に割り込んで、今何処を 実行中ねんって記録するやつ。この為にタイマーは必須。
どこを実行中ねん?の所は、継続を使えば取得出来るかな。まあ、先にタイマーの方を 見ておく。マニュアルに、Timersと いうずばりのものが有った。
* (schedule-timer (make-timer (lambda () (write-line "Hello, world") (force-output))) 2) * Hello, world
起動して2秒後に、印字されたけど、後はなしのつぶて。継続するにはどうする? そんなの考えるより、生きた例が有るだろうに。コードを嫁。
(setf *timer* (make-timer #'thread-distribution-handler :name "SB-PROF wallclock timer" :thread *timer-thread*)) (schedule-timer *timer* sample-interval :repeat-interval sample-interval)))
余計なキーワードが 付いていたので、習ってみる。
* (schedule-timer (make-timer (lambda () (write-line "Hello, world") (force-output))) 2 :repeat-interval 2) * Hello, world Hello, world Hello, world Hello, world Hello, world Hello, world :
タイマーを止めるのは、stop-profiling にあるように、
(:time (unschedule-timer *timer*) (setf *timer* nil *timer-thread* nil)))
タイマー起動時の仕事に名前を付けておいて、そいつをキャンセルすればいいんだ。 スレッドを殺すのもnilにすれば、いいのね。段々分かってきたよ。
スレッドの扱いはどうよ。マニュアルを見ると、関数が羅列されてるけど、どうやって 組み合わせればいいのやら?
基本に立ち返ってみる。ってんで、手元にある On Lispをパラパラしてたら、興味深い 章が有った。マルチプロセスですって。取り合えず読んでみるか。
forkとかwaitとかkillとか、思わずOSの教科書かいなと思っちゃったぞ。で、その 根底には継続があるのね。前の章で継続が扱われていた。
継続は力なり
で、On Lispの継続の章も読んでみた。継続と言ったらそりゃ本家はschemeでしょう。 ポール・グレアムさんもそこの所は承知していて、最初にSchemeで説明してた。
ついでに、SchemeとCLの違いにも言及してたぞ。Lisp-1とLisp-2の違い、変数と関数の 登録空間が一つしかないSchemeと、別々になってるCL。別々になってるCLでは、ぽいと シンボルが出てきた場合、変数か関数か分からない。だから、#' とか、頭に付けて 、関数だよーって自己主張せよとな。Scheme人間には、ちと鬱陶しい思いがする。
ポールも、どちらが良いとは弾言してない。宗教戦争になるからね。
おいらも、継続を復習しておくかな。 Scheme処理系の制作 第3回 継続とは が、面白かったよ。
何はともあれ、継続は力なり、数年おきに、各言語を巡っているけど、経験値が上昇して くるんで、そのたびに発見があるな。
clojure + cider
毎回、CLを入れて行く短期企画。今回は、clojureです。あれ? clojureってCLちゃうん と言う、突っ込みは無しの方向で。まあ、何回もcljを入れてるんで、またかですが。
lein を取ってきて、PATHを通しておきます(実行属性の付与も忘れずにね)。後は適当に
[sakae@manjaro ~]$ lein new testclj
とかして、cljure一族を取ってきます。emacsからも便利に使いたかったら、もう一手間 かけておきます。
[sakae@manjaro ~]$ cd testclj/ [sakae@manjaro testclj]$ vi project.clj : (defproject testclj "0.1.0-SNAPSHOT" :description "FIXME: write description" :plugins [[cider/cider-nrepl "0.7.0-SNAPSHOT"]] ;; <-- 追加 : [sakae@manjaro testclj]$ lein deps
こうすると、cider系の便利ツールも読み込まれます。そして、emacsから 補完が使えるようになります。ここまででちょっと動作確認。
[sakae@manjaro testclj]$ lein repl nREPL server started on port 39334 on host 127.0.0.1 REPL-y 0.3.1 Clojure 1.5.1 Docs: (doc function-name-here) (find-doc "part-of-name-here") Source: (source function-name-here) Javadoc: (javadoc java-object-or-class-here) Exit: Control+D or (exit) or (quit) Results: Stored in vars *1, *2, *3, an exception in *e user=>
続いて、emacsから扱えるようにします。 最近は、ciderって言う微妙な名前のやつもあるみたい。 Clojureの開発環境CIDERとか Clojure + emacs + cider に移行しました を、参考にしました。
elispのパッケージを取ってくるだけなんですが、何処にあるかの情報を、init.elに 書いて、リフレッシュしときます。
(require 'package) (add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/")) (add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t) (package-initialize)
後は、emacsから src/testclj/core.clj を読み込み M-x cider-jack-inするだけ なんですが、 remove-dos-eolなんてシンボルは無いって怒られた。何処で使ってるかと調べたら、init.elの 中に、(add-hook 'cider-mode-hook 'remove-dos-eol)が有った。知らずに人様の設定を 頂いてきた報いでしょうかな。フックなんで、そんなの無かった事にしてもいいんだけど、 頑張ってみます。
stackoverflowに溢れたコード例が出てましたんで、頂いてきました。
(defun remove-dos-eol () "Do not show ^M in files containing mixed UNIX and DOS line endings." (interactive) (setq buffer-display-table (make-display-table)) (aset buffer-display-table ?\^M []))
removeじゃ無くて見えなくしてるだけ? まあいいや。
; CIDER 0.7.0alpha (package: 20140620.753) (Java 1.7.0_60, Clojure 1.5.1, nREPL 0.2.3, cider-nrepl 0.7.0-snapshot) user>
ソース画面で、C-c C-k して、コンパイル後
user> (in-ns 'testclj.core) ;;=> #<Namespace testclj.core> testclj.core> (foo "hoge") hoge Hello, World! ;;=> nil
簡単に、clojureがemacs配下に収まってくれました。終了は、C-c C-q みたいなんだけど、 これってslimeの伝統コマンド、,sayoonara を破っていないかい。
これで、トレンドに追いついたかな?