移植
前回のlibm実験の時、OpenBSDにgambit-cをやむなく入れた。だって、gaucheが入らない んだもの。で、折角入れたんだから、一過性のものにしないで何か動かしたい。
そこで考えた。SECDマシンがgauche上で動いていたけど、これをgambit-cに移植して、 C語でコンパイルして単独アプリが出来ないかと。
いきなり、コンパイルして、足りない所を教えて貰おう。
[ob: tcall36]$ gsc -exe vm.scm *** WARNING -- "list*" is not defined, *** referenced in: ("/home/sakae/secd/tcall36/vm.c") *** WARNING -- "list-copy" is not defined, *** referenced in: ("/home/sakae/secd/tcall36/vm.c") *** WARNING -- "pop!" is not defined, *** referenced in: ("/home/sakae/secd/tcall36/vm.c") *** WARNING -- "push!" is not defined, *** referenced in: ("/home/sakae/secd/tcall36/vm.c")
ワーニングを出しつつも、vmって名前のアプリが出来上がった。果たして動くか? 半信半疑ながら実行してみる。
[ob: tcall36]$ ./vm >>> (define aa 1) *** ERROR IN comp -- Operator is not a PROCEDURE (#!unbound 'def 'aa '(stop))
そして、うな垂れる。確信犯なんだけど、コンパイラーは何故エラーにせずにワーニングという 弱気な警告しか出さなかったの? 多分複数のファイルをコンパイルして、それを合体 する事もあろうに。そういう時は、一つのファイルだけに注目して、関数が定義してない ってエラーには落とせない。だから、せめて警告だけでもって事か。
まあ、いずれにしても、足りないものを補う必要が有る。gaucheのマニュアルを 調べると、list*は、srfi-1のcons*と同じもの。push/popはcommonLispから借用して きたとか書いてある。list-copyはsrfi-1に定義される、便利なやつなんだけど、gambit には無いやつ。
そういう調べの元、srfi-1.scm から、必要な物を頂いてくる。残るは、push/popだけど、これはもう、マクロだろうって 当りをつける。
果たして、gambitでマクロ機能なんて使えるの? こういう時は、infoするんだった。 マクロ定義が出来ると書いてあった。例でpushが出てた。後はpopを用意すればいいなってんで、 追加した。本当はgemsymで保護が必要だけど、取りあえず無視。下記を追加した。
(define (list* first . rest) (let recur ((x first) (rest rest)) (if (pair? rest) (cons x (recur (car rest) (cdr rest))) x))) (define (list-copy lis) (let recur ((lis lis)) (if (pair? lis) (cons (car lis) (recur (cdr lis))) lis))) (define-macro (push! var #!optional val) `(set! ,var (cons ,val ,var))) (define-macro (pop! var) `(let ((head (car ,var))) (set! ,var (cdr ,var)) head))
これで移植完了かな?
[ob: tcall36]$ time gsc -exe vm.scm 0m9.48s real 0m2.64s user 0m6.48s system [ob: tcall36]$ ./vm >>> (define fib (lambda (n) (if (< n 3) n (+ (fib (- n 1)) (fib (- n 2)))))) ldfld00ldc3args2ldg<appselrld00rtnld00ldc1args2ldg-appargs1ldgfibappld00ldc2args2ldg-appargs1ldgfibappargs2ldg+tapprtndeffibstop(time (vm '() '() expr '())) 0 ms real time 0 ms cpu time (0 user, 0 system) no collections 264 bytes allocated 1 minor fault 9 major faults fib >>> (fib 30) ldc30args1ldgfibappstop(time (vm '() '() expr '())) 8174 ms real time 8170 ms cpu time (5150 user, 3020 system) 1897 collections accounting for 518 ms real time (40 user, 380 system) 2036832352 bytes allocated 289 minor faults 4 major faults 1346269
コンパイラー版だから、スイスイ動くかと思ったら、のっそりした動きだな。
[ob: tcall36]$ gsi vm.scm >>> (fib 30) ldc30args1ldgfibappstop(time (vm '() '() expr '())) 146575 ms real time 146520 ms cpu time (122090 user, 24430 system) 13084 collections accounting for 9559 ms real time (1980 user, 7870 system) 10964420600 bytes allocated 225 minor faults no major faults 1346269
インタープリタは押して知るべしだ。でも、traceを自前で持ってるぞ。
> (trace vm) > (repl) >>> (define hoge (lambda (x) (* x x x))) | > (vm '() '() '(ldf (ld (0 . 0) ld (0 . 0) ld (0 . 0) args 3 ldg * app rtn)... | > (vm '((closure (ld (0 . 0) ld (0 . 0) ld (0 . 0) args 3 ldg * app rtn) ()... | > (vm '(hoge) '() '(stop) '()) | hoge hoge >>> (hoge 5) | > (vm '() '() '(ldc 5 args 1 ldg hoge app stop) '()) | > (vm '(5) '() '(args 1 ldg hoge app stop) '()) | > (vm '((5)) '() '(ldg hoge app stop) '()) | > (vm '((closure (ld (0 . 0) ld (0 . 0) ld (0 . 0) args 3 ldg * app rtn) ()... | > (vm '() '((5)) '(ld (0 . 0) ld (0 . 0) ld (0 . 0) args 3 ldg * app rtn) '... | > (vm '(5) '((5)) '(ld (0 . 0) ld (0 . 0) args 3 ldg * app rtn) '((() () (s... | > (vm '(5 5) '((5)) '(ld (0 . 0) args 3 ldg * app rtn) '((() () (stop)))) | > (vm '(5 5 5) '((5)) '(args 3 ldg * app rtn) '((() () (stop)))) | > (vm '((5 5 5)) '((5)) '(ldg * app rtn) '((() () (stop)))) | > (vm '((primitive #<procedure #2 *>) (5 5 5)) '((5)) '(app rtn) '((() () (... | > (vm '(125) '((5)) '(rtn) '((() () (stop)))) | > (vm '(125) '() '(stop) '()) | 125 125
そしてdebug関連も充実してる。(help-browser "w3m") (help break)とかでも良い。 w3mを指定せずに使いたい時は、/usr/local/bin/gambc-doc を事前に編集しておく。
> ,? ,? : Summary of comma commands ,h | ,(h X) : Help on procedure of last error or procedure/macro named X ,q : Terminate the process ,qt : Terminate the current thread ,t : Jump to toplevel REPL ,d : Jump to enclosing REPL ,c | ,(c X) : Continue the computation with stepping off ,s | ,(s X) : Continue the computation with stepping on (step) ,l | ,(l X) : Continue the computation with stepping on (leap) ,N : Move to specific continuation frame (N>=0) ,N+ | ,N- : Move forward/backward by N continuation frames (N>=0) ,+ | ,- : Like ,1+ and ,1- ,++ | ,-- : Like ,N+ and ,N- with N = nb. of frames at head of backtrace ,y : Display one-line summary of current frame ,i : Display procedure attached to current frame ,b | ,(b X) : Display backtrace of current continuation or X (cont/thread) ,be | ,(be X) : Like ,b and ,(b X) but also display environment ,bed | ,(bed X) : Like ,be and ,(be X) but also display dynamic environment ,e | ,(e X) : Display environment of current frame or X (proc/cont/thread) ,ed | ,(ed X) : Like ,e and ,(e X) but also display dynamic environment ,st | ,(st X) : Display current thread group, or X (thread/thread group) ,(v X) : Start a REPL visiting X (proc/cont/thread)
ああ、emacs用のコードも提供されてるんで、(require 'gambit)しておいて、run-scheme で行ける。
gambit vs. gauche
オイラーの感覚では、gsc版がもっと速いと思っていたんだけど、どうなのよ? こういう時は、同じ土俵で比べるのがベターです。この俺がルールだとか、 ルールを勝手に変更してしまう某スポーツ界みたいなことは、いけません。
舞台をFreeBSDに移して、gaucheと比較します。 まずは、基準走のgauche君から。
>>> (fib 30) (ldc 30 args 1 ldg fib app stop) ;(time (vm '() '() expr '())) ; real 24.533 ; user 24.352 ; sys 0.078 1346269
gscでコンパイルした、SECDマシン
>>> (fib 30) ldc30args1ldgfibappstop(time (vm '() '() expr '())) 9717 ms real time 9682 ms cpu time (8411 user, 1271 system) 1944 collections accounting for 881 ms real time (867 user, 587 system) 2036832352 bytes allocated 242 minor faults no major faults 1346269
gsiのインタープリタ版
>>> (fib 30) ldc30args1ldgfibappstop(time (vm '() '() expr '())) 127598 ms real time 127005 ms cpu time (119726 user, 7278 system) 13549 collections accounting for 12405 ms real time (11710 user, 3872 system) 10963674080 bytes allocated 179 minor faults no major faults 1346269
スピードを稼ぐなら当然のごとく、コンパイル版。debugの充実を求めるならgsi。 バランスがよく、何でも有りがgaucheと、予想通りの結果となりました。
cexpl
前回、FreeBSDでオイラーの式を精度よく計算しようとしたら、エラーで刎ねられた。
$ cc t.c -lm t.c:11:8: warning: implicitly declaring library function 'cexpl' with type '_Complex long double (_Complex long double)' z = cexpl(x); ^ t.c:11:8: note: please include the header <complex.h> or explicitly provide a declaration for 'cexpl' 1 warning generated. /tmp/t-fd3182.o: In function `main': t.c:(.text+0x90): undefined reference to `cexpl' cc: error: linker command failed with exit code 1 (use -v to see invocation)
これが、clangでの結果。曖昧だから、ちゃんと宣言してね。どうかcomplex.hを使って くれってともご宣託。そのくせスパッとエラーに落とされた。
$ gcc48 t.c -lm t.c: In function 'main': t.c:11:8: warning: incompatible implicit declaration of built-in function 'cexpl' [enabled by default] z = cexpl(x); ^ /tmp//ccBpR7vY.o: 関数 `main' 内: t.c:(.text+0x7a): `cexpl' に対する定義されていない参照です collect2: error: ld returned 1 exit status
gccでも、ほぼ同様な事を言ってきた。libmがcexpl()を提供してないんだから、当然の 報い。ならば、OpenBSDから、輸血してもらえばいいんでないかい。あれ? 輸血って 広義の移植なんでしょうか? >医療関係者殿
オイラー的には、血も大事な臓器の一種だと思うから、移植に同意。もしものために 備えて、貯血しておきましょう。人様から血を分けて貰って、同時に肝炎とかHIVも 分けて貰ったんじゃ、本末転倒ですから。
ってな事で、OpenBSDから、分けてもらう。分けて貰った結果は下記。
1 #include <stdio.h> 2 #include <complex.h> 3 #include <math.h> 4 5 long double complex 6 cexpl(long double complex z) { 7 long double complex w; 8 long double r; 9 10 r = expl(creall(z)); 11 w = r * cosl(cimagl(z)) + (r * sinl(cimagl(z))) * I; 12 return (w); 13 } 14 15 int main(void){ 16 long double _Complex z, x; 17 18 x = I * 2 * acosl(0); 19 z = cexpl(x); 20 21 printf("real=%g\timag=%38.36g\n", creal(z),cimag(z)); 22 23 return 0; 24 }
コピペしたんで、本当はクレジットも表記しないとイケンけど許してね。このままだと、 ただのコピペ少年ならぬ、コピペ老人と思われちゃうんで、コードを観賞してみる。 10行目で、実部のべき乗を得る。11行目には、オイラーさんが潜んでいる。cimagl(z)は、 偏角になるんで、オイラー式の一般形となる。
今度は、2人の執刀医による確認でも、文句無く合格。実行もちゃんと出来た。 移植成功で万歳三唱ですかね。
$ cc -g t.c -lm $ gcc48 -g t.c -lm $ ./a.out real=-1 imag=1.2246467991473532071737640294583966e-16
まてまて、結果を注意深く点検するんだ。OpenBSDとかFedoraの結果と比べてミレ。
real=-1 imag=-5.01655761266833226942395050995492469e-20 ;; OpenBSD real=-1 imag=-5.01655761266833226942395050995492469e-20 ;; Fedora real=-1 imag=1.2246467991473532071737640294583966e-16 ;; Debian/ARM
これ、前回の結果から引っ張ってきた。仮数も指数もDebian/ARM 症候群に なってますねぇ。似非、移植が行われたって事?
こんな事もあろうかと、gdb用のプローブを埋め込んでおいたのさ。迷医は、もしも の事も考えておくものです。ブラック・ジャックみたいな天才医はどうなんでしょ。 また、読んでみたいなーー。
で、早速、比較健闘もとえ検討です。医学も正常なものをたくさん集めておいて、対象と 比べる事により進化してますからね。
まずは、正常系のOpenBSD
Breakpoint 1, cexpl (z=0 + 3.1415926535897932385128089594061862 * I) at /usr/src/lib/libm/src/s_cexpl.c:66
次は異常なFreeBSD
Breakpoint 1, cexpl (z=-1.3222456819757518088907606594413914e+861 + 0 * I) at t.c:10
計算すべき値がcexplに届いていません。ならば、cexplへの引数を直接指定してみます。 OpenBSDと比較出来るようにcexplはhogeと改名しておきます。
x = 0.0 + M_PI * I; z = hoge(x);
これで、clangでコンパイルしてgdbにかけてみると
Breakpoint 1, hoge ( z=2.4473971540385850596540858107266629e-4942 + <invalid float value> * I) at t.c:10 10 r = expl(creall(z)); (gdb) up #1 0x0804879a in main () at t.c:19 19 z = hoge(x); (gdb) p x $1 = 0 + 3.1415926535897931159979634685441852 * I
どうも、引数が正しく渡っていないようです。ならば、同じ事をgcc48でやってみると 、正しく渡ってきました。
Breakpoint 1, hoge (z=0 + 3.1415926535897931159979634685441852 * I) at t.c:10 10 r = expl(creall(z));
結局、clangのBugを踏んだって事かな。検証は、糞石レベルまで下りないと出来ない から、これ以上は止めておこう。後は、精度問題。
上で与えてるM_PIでは、精度が取れないようで、OpenBSDでやると、e-16の精度に なる。そこで、x = 0.0 + (2 * acosl(0)) * I; を復活させる。
で、じっとcexplを眺めてみると、虚部に関する精度の肝は、sinlに委ねられているって 予想される。よって、その部分を取り出して検証出来るようにしておく。
long double complex hoge(long double complex z) { long double complex w; long double ci, sv, r; ci = cimagl(z); sv = sinl(ci); :
z=0 + 3.1415926535897931159979634685441852 * I ;; FreeBSD z=0 + 3.1415926535897932385128089594061862 * I ;; OpenBSD ^^^^^^^^^^^^^^^^^^^ z=0 + 3.1415926535897931159979634685441852 * I ;; 0.0 + M_PI * I (gdb) p ci $1 = 3.1415926535897931159979634685441852 ;; FreeBSD $1 = 3.1415926535897932385128089594061862 ;; OpenBSD ^^^^^^^^^^^^^^^^^^^ (gdb) p sv $2 = 1.2246467991473531772014792699320386e-16 ;; FreeBSD $2 = -5.0165576126683320234517576003912636e-20 ;; OpenBSD
どうも、OpenBSDでは、acoslの精度が悪いようだ。そして、そのいかがわしい値を 使って計算した結果が、たまたま誤差が少なくなったものだから、安心しちゃった んだな。
まとめると、sinl(π)を正確に求めましょ。言い換えるとsin(180°)って事だから、 期待値は、中学生でも知ってる答え ゼロ。そのπをacosから導出したものだから、 正しいπにならなかった。誤差が相殺して、たまたま精度よく答えが得られたと 錯覚した。fedoraもOpenBSDと同じ結果だったので、多数決も味方したって事だな。
いやー、オイラーさんは色々な事を勉強させてくれるな。
clangの弁明
上で、clangを使ってコンパイルした時、cexplに引数が正しく渡っていないってサジを 投げてしまった。でも、改めて見直すと、どうやらそうとも言えないと思える。
$ cc -g -O0 t.c -lm $ gdb -q a.out Reading symbols from a.out...done. (gdb) b hoge Breakpoint 1 at 0x8048692: file t.c, line 10. (gdb) run Starting program: /usr/home/sakae/z/a.out Breakpoint 1, hoge (z=<invalid float value> + <invalid float value> * I) at t.c:10 10 ci = cimagl(z); (gdb) n 11 sv = sinl(ci); (gdb) 13 r = expl(creall(z)); (gdb) 14 w = r * cosl(cimagl(z)) + (r * sinl(cimagl(z))) * I; (gdb) p z $1 = <optimized out> (gdb) p ci $2 = 3.1415926535897931159979634685441852 (gdb) p sv $3 = 1.2246467991473531772014792699320386e-16 (gdb) p r $4 = 1 (gdb) c Continuing. real=-1 imag=1.2246467991473532071737640294583966e-16
hoge(cexpl)に渡ってくる引数は、clang流に解釈すれば定数。だから、関数内ではオプチマイズ されててそのままでは参照出来んと言ってるんだな。gdbでbreakした時は、gdbの 解釈に齟齬があって、invalidって言ってるのだろう。 cimaglではきちんと値が取り出せている。また、creallの値は直接には確認して いないけど、計算結果から容易に0と推測される。
ちょっと、objdumpして、該当箇所を覗き見してみる。
x = 0.0 + M_PI * I; 8048763: db bc 24 98 00 00 00 fstpt 0x98(%esp) 804876a: dd 05 d0 88 04 08 fldl 0x80488d0 8048770: d9 c0 fld %st(0) 8048772: db bc 24 a4 00 00 00 fstpt 0xa4(%esp) z = hoge(x); 8048779: db ac 24 98 00 00 00 fldt 0x98(%esp) 8048780: db 7c 24 68 fstpt 0x68(%esp) 8048784: db 7c 24 74 fstpt 0x74(%esp) 8048788: 8b 4c 24 7c mov 0x7c(%esp),%ecx :
インテルのマニュアルから動作を確認しようとしたけど微妙にニューモニックが異なる。 でも、定数を直接x87に転送してるっぽい。動作の確認は面倒なので、gdbに任せちゃえ。
Breakpoint 1, main () at t.c:21 21 x = 0.0 + M_PI * I; (gdb) n 22 z = hoge(x); (gdb) info float =>R7: Valid 0x4000c90fdaa22168c000 +3.141592653589793116 R6: Empty 0x4000c90fdaa22168c000 R5: Empty 0x00000000000000000000 R4: Empty 0x00000000000000000000 R3: Empty 0x00000000000000000000 R2: Empty 0x00000000000000000000 R1: Empty 0x00000000000000000000 R0: Empty 0x00000000000000000000 Status Word: 0x3800 TOP: 7 Control Word: 0x127f IM DM ZM OM UM PM PC: Double Precision (53-bits) RC: Round to nearest Tag Word: 0x3fff Instruction Pointer: 0x33:0x08048772 Operand Pointer: 0x3b:0xbfbfe804 Opcode: 0xdbbc (gdb) display/i $pc 1: x/i $pc => 0x8048779 <main+57>: fldt 0x98(%esp)
思った通り、0x08048772番地の命令(hogeへの突入直前)を実行した後、パイの値が FPUに転送されている。これじゃ、gdbが右往左往したってしょうがないか。虚部の パイがFPUの載るのは良いとして実部の0はどうするんだろう? そこがちと疑問。
hogeの中をちょっと変更してから、コードを覗いてみる。
hoge(long double complex z) { 8048680: 55 push %ebp 8048681: 89 e5 mov %esp,%ebp 8048683: 81 e4 f8 ff ff ff and $0xfffffff8,%esp 8048689: 81 ec 80 00 00 00 sub $0x80,%esp 804868f: 8d 45 0c lea 0xc(%ebp),%eax 8048692: 8b 4d 08 mov 0x8(%ebp),%ecx long double complex w; long double ci, cr, sv, r; cr = creall(z); 8048695: db 28 fldt (%eax) 8048697: db 7c 24 50 fstpt 0x50(%esp) ci = cimagl(z); 804869b: db 68 0c fldt 0xc(%eax) 804869e: d9 c0 fld %st(0) 80486a0: db 7c 24 5c fstpt 0x5c(%esp)
どうやらzのデータは、ポインターで渡ってきているな。そして実部も虚部も、 一度FPUに載せて、それをfstptで、所定のローカルエリアに転送してる。 こういうのも有りなんだろうけど、そんな作法はGNUの世界では一般的じゃ ないので面倒見れんと言う事か。あー、すっきりした。
FreeBSDのデフォルトccがclangになって、皆さんdebugには苦労していないんですかね? オイラーみたいな軟弱者のために、clangとタイアップしたlldbがリリース されないかな?
後は、折角オイラーさんが、libmの中に潜んでおられるので、よくそのお姿を 拝んでおけ。