敏捷さとダイエット
女房が 蒟蒻畑 を買ってきた。いつも間に復活していたんだろう? あのつるっと した感触が堪らない。また買ってきてねとリクエストしてしまった。
同じ蒟蒻でも、ご飯に入れて、量は増えるけどご飯の分量は減っているというやつ は、勘弁願いたい。これって、ダイエットに効果があるんだろうか? 某、コマーシャルで 数日前から見せられている、サングラスの人がぱっと服を脱いで、自慢?のボディ を披露するのも、願い下げ。
胸よりも 前に出るなと 腹に言う
気持ち、分かるけどさ。
敏捷さ
ふと、昨日のコードに敏捷さがあるんだろうかと疑問が沸いてきた。 気になったのは、この部分
(cordec-write (fold + 0.0 (map * *xn* *hm*)))
map すると結果はリストで返ってくるけど、すぐに消費されてしまって、作られた リストは「ごみ」になってしまう。ごみが出れば、ごみ掃除が発動されて、かくっ、かくっ となるのではないか。強いては、敏捷さに欠けるものになってしまうと予測になった。 こういうのは、考えても分からないので、実験してみる。
(define hm (make-list 1000000 1)) (define xn (make-list 1000000 1)) (define (com) (fold (lambda (x y sum) (+ sum (* x y))) 0 hm xn)) (define (sep) (fold + 0 (map * hm xn)))
mapを無くしたものを書いてみた。gaucheのinfoを見たら、同様な例が載っていた 事からすると、有名な使い方なのだろう。ベクトルの内積と言うそうだ。
敏捷さを計るので、timeだな。
gosh> (time (com)) ;(time (com)) ; real 7.016 ; user 6.781 ; sys 0.023 1000000 gosh> (time (sep)) ;(time (sep)) ; real 5.479 ; user 5.359 ; sys 0.016 1000000
意に反して、mapを使った方が早かった。何度か実験してみたけど、優位な差(20%以上 速い)がある。これは、何故だろう? どんな風にコンパイルされてるか確認して みよう。
gosh> (disasm com) main_code (name=com, code=0x8154f90, size=11, const=4, stack=7): args: #f 0 CLOSURE #<lambda 0> ; (lambda (x y sum) (+ sum (* x y))) 2 PUSH 3 CONSTI-PUSH(0) 4 GREF-PUSH #<gloc user##hm>; hm 6 GREF-PUSH #<gloc user##xn>; xn 8 GREF-TAIL-CALL(4) #<gloc gauche##fold>; (fold (lambda (x y sum) (+ sum (* x y))) ... 10 RET internal_closure_0 (name=#f, code=0x80e2df8, size=6, const=0 stack=2): args: #f 0 LREF0-PUSH ; sum 1 LREF2-PUSH ; x 2 LREF1 ; y 3 NUMMUL2 ; (* x y) 4 NUMADD2 ; (+ sum (* x y)) 5 RET
gosh> (disasm sep) main_code (name=sep, code=0x8105f80, size=16, const=6, stack=15): args: #f 0 GREF-PUSH #<gloc scheme##+>; + 2 CONSTI-PUSH(0) 3 PRE-CALL(3) 13 5 GREF-PUSH #<gloc scheme##*>; * 7 GREF-PUSH #<gloc user##hm>; hm 9 GREF-PUSH #<gloc user##xn>; xn 11 GREF-CALL(3) #<gloc scheme##map>; (map * hm xn) 13 PUSH-GREF-TAIL-CALL(3) #<gloc gauche##fold>; (fold + 0 (map * hm xn)) 15 RET
コンパイルされたコードのサイズは同じであるけど、前者の方は、lambdaで定義した 無名関数が呼び出されるように見える。internal_clojure_0 が、リストのlength回数 呼び出されるので、そのオーバーヘッドが無視出来ないのだろう。
ダイエット
そんじゃ、fold . map の方が絶対的に有利かというと、そうでもない。 そもそもの発端となった危惧が、メモリー消費と言う面で顕著化してくるのだ。 (みみっちい話と言えばそれまでだが、)
PID USERNAME THR PRI NICE SIZE RES STATE TIME WCPU COMMAND 2072 sakae 1 5 0 4568K 3444K ttyin 0:00 0.00% gosh 2072 sakae 1 5 0 22656K 19468K ttyin 0:01 1.27% gosh 2072 sakae 1 5 0 37440K 36200K ttyin 0:08 14.16% gosh 2090 sakae 1 5 0 48572K 47256K ttyin 0:06 19.64% gosh 2090 sakae 1 5 0 48572K 47300K ttyin 0:33 31.20% gosh
上のデータは、top の結果を抜き出して、貼り付けたものである。(emacsからgosh を起動してます)上から順番に
- goshを起動直後
- スクリプトをロード(C-c C-l)
- com を実行後
- 1. 2. を実行後に sep を実行
- 4.の後、数度 sep を実行
この結果をどう使うか(使わないって)は、ご自由に。
それにしても、100万回の積和演算が、5秒って、速いんですかね? そりゃ、DSP には負けるに決まってますが。
おまけ
Windowsマシンに乗っている petite上でも走らせてみた。(fold が無かったので fold-leftに 変更した上で)
> (time (com)) (time (com)) 4 collections 350 ms elapsed cpu time, including 0 ms collecting 357 ms elapsed real time, including 0 ms collecting 16000584 bytes allocated, including 29058904 bytes reclaimed 1000000 > (time (sep)) (time (sep)) 2 collections 291 ms elapsed cpu time, including 70 ms collecting 296 ms elapsed real time, including 67 ms collecting 14465896 bytes allocated, including 3780224 bytes reclaimed 1000000
やはり敏捷さと言う側面では、gaucheと同じだけど、メモリー負担の面では、分解 しておいた方が速いという結果になった。処理系に個性が出るのね。
さらに^2
VMWARE上のFreeBSDに乗っている ikarus では、
> (time (com)) running stats for (com): 23 collections 342 ms elapsed cpu time, including 10 ms collecting 352 ms elapsed real time, including 19 ms collecting 96000008 bytes allocated 1000000 > (time (sep)) running stats for (sep): 6 collections 3602 ms elapsed cpu time, including 2550 ms collecting 3682 ms elapsed real time, including 2616 ms collecting 21953840 bytes allocated 1000000
ふぅーー。