golang
昭和の香りがプンプンする『寺内貫太郎一家』(新潮社)なんてのを読んでみた。
向田邦子さんが書かれて、1974年にTBSからTVドラマになったんで、ご記憶の方も多かろう。 平均視聴率31%、最終回は42%だったそうだから、大したものだ。
寺内貫太郎一家&東芝日曜劇場 とか マンガ・アニメの間取り『寺内貫太郎一家』 なんてのもネットに有った。勿論、違法物のやつもあるけど、蔦谷あたりからDVDを借りて きてみてくらはい。あるいは、TBSのオンデマンドでもいいけど。おいらの所は、TVを ネットにつなげていないので(だって感染して、毎日AVじゃ困るから)見れない。
上記の定本あたりを読むのがretroな人にはお似合い。ipadとかで読むなんてのは、 もっての他ですよ。
週間高視聴率番組10なんてのを 見ると、毎週同じ番組が並んでいるけど、日曜劇場・半沢直樹が割りに良い視聴率を 叩きだしているな。そのうちに、この番組もシリーズ化されて、2匹目のドジョウを 狙うのだろうか? 手堅くやりたいのはどこも一緒かな。
詐欺に遭った?
以前にやったretroのベンチ。Cで書かれたVMよりもgoで書かれたVMの方が、圧倒的に 速かった。あの時は、丁度ベル研の本を読んでいたんで、ベルのマジックと持てはやし ちゃったけど、冷静に考えたら、おかしいよね。そんな旨い話が有れば、今やgoが デファクトスタンダードになっているはず。
あの時は舞台がFreeBSD上だったけど、今度はfedoraに場所を変えてやってみる。素材は、 単にループするやつ。
[sakae@fedora benchmarks]$ time ../a.out real 0m0.650s user 0m0.637s sys 0m0.007s
まずは、アセンブラー職人の手による驚異の出来栄えをご覧下さい。さすが、手間隙かかって いるだけの事はありますなあ。
[sakae@fedora benchmarks]$ time ../retro real 0m4.082s user 0m3.773s sys 0m0.274s [sakae@fedora benchmarks]$ time ../main real 0m2.269s user 0m2.187s sys 0m0.060s
いろいろな言語のベンチマークサイト で go vs. cの結果を見ると、 課題にもよるけど、Cより10倍も時間がかかるものも見受けられる。こういう証拠が 有るんで、詐欺に遭ったかと不安になるんだ。(-- 気が付くのが遅いんだよ。)
そして、goのFAQ でも、正直に遅いよと告白してるしね。でも、世の中、スピードだけが正義じゃ無いよと 唱えている人が沢山いる。ほら、あそこにも居るでしょ。
幾ら実行スピードが速くたって、書くのに苦労するんじゃねぇって事で、LLな訳だ。 そこで、 スピード対コード行数 を散布図にしたのが公開されてる。縦軸がスピード、横軸が行数になる。理想は、どんな 例題でも左下に固まる事だ。なかなか理想な言語は無いねぇ。バランスが良いと思うのは、 Ocamlぐらいですか。
go bench
goのFAQで知ったんだけど、ソースの配布物にベンチコードも入っているのね。早速行って みよーーー。
ベンチの自動化スクリプトが用意されてたので走らせてみると、hgってのが必要らしい。 hgって何よ? 調べてみたら、Pythonで書いたgitクローンらしい。ググル社内では、 身内のプロダクトを使えって言う掟でもあるんでしょうか?
入れてみたけど、hgのエラーが更に発生するんで、めんどくさくなり、自動化スクリプト の冒頭付近をちょいと改変。
set -e ## eval $(go tool dist env) ## go 任せはgo面 O=8 ## おいらは、未だに i686 ユーザーさ GC="go tool ${O}g" LD="go tool ${O}l"
これで走るようになったんで、目ぼしいものを拾ってみた。
[sakae@fedora shootout]$ ./timing.sh fasta -n 25000000 gcc -m32 -O2 fasta.c 2.52u 0.07s 2.62r gc fasta 2.07u 0.06s 2.16r gc_B fasta 2.23u 0.09s 2.34r binary-tree 15 # too slow to use 20 gcc -m32 -O2 binary-tree.c -lm 0.97u 0.01s 1.01r gc binary-tree 2.58u 0.19s 2.88r gc binary-tree-freelist 0.43u 0.01s 0.46r mandelbrot 16000 gcc -m32 -O2 mandelbrot.c 43.43u 1.77s 46.13r gc mandelbrot 101.51u 3.87s 107.58r gc_B mandelbrot 104.40u 1.75s 108.48r pidigits 10000 gcc -m32 -O2 pidigits.c -lgmp 3.54u 0.16s 3.80r gc pidigits 8.14u 0.79s 9.19r gc_B pidigits 7.99u 0.96s 9.18r
FAQが言うように、圧倒的に遅いね。
でも、現実に戻って、retroを走らせるとやはりfedoraでもgoは速かった。何故何故? 刑事さんになった積もりで 推測してみよーー。
現場検証
Cで書かれたretroには、statsなんてオプションが用意されてる。これを付けて起動すると、 retroが終了した時に、VMのステップ数が表示される。
[sakae@fedora benchmarks]$ ../retro --stats Runtime Statistics NOP: 0 LIT: 40452 DUP: 10050504 DROP: 20203 : DEC: 10050504 IN: 6 OUT: 17 WAIT: 8 CALL: 20131331 Max SP: 6 Max RSP: 24 Total opcodes processed: 120656660
それぞれのVM命令の実行回数と、データスタック、リターンスタックの最大使用深さ、そ して、トータルのVM命令実行回数。
こういうデータはgo側のコードでは収集していない。C側のコードでは、このデータを 収集する・しないの判定が行われているんだな。C側は生まれながらにして、ハンディを 背負っている事になる。何せ、120656660回も余分な判定してるんで、これが効いてるんでは なかろうか?
void rxProcessOpcode(VM *vm) { CELL a, b, opcode; opcode = vm->image[IP]; /* if (opcode > NUM_OPS) vm->stats[NUM_OPS]++; else vm->stats[opcode]++; */ switch(opcode) { case VM_NOP: break;
上記のように、命令数を数えてる部分をコメントにしてみた。そして、その結果は
[sakae@fedora benchmarks]$ time ../retro real 0m3.106s user 0m3.036s sys 0m0.037s
1秒ぐらい早くなったな。やっぱり余計な荷物を背負っていたんだな。でも、まだ隠れて 荷物が有りそう。 調べてみると、スタックの深さを監視してるコードが、あちこちに埋め込まれいた。一つ 例を上げると、
case VM_DUP: SP++; vm->data[SP] = NOS; // if (vm->max_sp < SP) // vm->max_sp = SP; break;
DUPするとスタックが深くなるんで、今までの深さを更新してないかチェックしてるんだな。 これらをコメントアウトして実行すると、
[sakae@fedora benchmarks]$ time ../retro real 0m3.273s user 0m3.124s sys 0m0.122s
Uum ... どうも、誤差範囲だなあ。もう他人任せにするかな。伝家の宝刀を抜いてみる。 ベンチする時は、みんなチューニングするのは当たり前。-O2を付けてコンパイル。
[sakae@fedora retro-11.5]$ gcc -v : gcc version 4.8.1 20130603 (Red Hat 4.8.1-1) (GCC) [sakae@fedora retro-11.5]$ make rm -f retro rm -f retroImage16 retroImage64 rm -f retroImage16BE retroImageBE retroImage64BE rm -f *~ gcc -Wall -O2 vm/complete/retro.c -o retro [sakae@fedora benchmarks]$ time ../retro : 114 0 0 0 1 100 5434 23054 1 100 5434 23052 1 1000 5434 23050 9 14150 9 14150 9 14150 19087 9 9 real 0m1.624s user 0m1.457s sys 0m0.053s
チューニングの副作用で、何やらデータがDumpされるようになっちゃったけど、劇的に 速くなったね。このデータがコンペ用の公式記録になるんでしょうな。goにも軽く勝って いるしね。
でも、ちょいと副作用が気になるんで、コンパイラをgccからclangに乗り換えてみます。
[sakae@fedora retro-11.5]$ clang -v clang version 3.3 (tags/RELEASE_33/rc3) Target: i386-redhat-linux-gnu Thread model: posix [sakae@fedora retro-11.5]$ make rm -f retro rm -f retroImage16 retroImage64 rm -f retroImage16BE retroImageBE retroImage64BE rm -f *~ clang -Wall -O2 vm/complete/retro.c -o retro [sakae@fedora benchmarks]$ time ../retro : 114 0 0 0 1 100 5434 23054 1 100 5434 23052 1 1000 5434 23050 9 14150 9 14150 9 14150 19087 9 9 real 0m1.852s user 0m1.665s sys 0m0.075s
今度は、clang vs. gcc の勝負になっちゃったよ。
golangへ移植
上では、Cが背負っていた荷を下ろして身軽にしたけど、逆にgo側に荷を背負わせたって いい。この際だから、C側でやってたstatsをgo側へ移植してみる。
How to Write Go Codeを見てると、さりげなく、.hg なんてのが出てくる。これってググルご推薦のバージョン管理用リポジトリィーだよね。
GOに入ってはGOに従え
と言うから、おいらもバージョン管理をやってみる。参考にしたのは、 Mercurialではじめる分散構成管理 とか Mercurial の利用 だ。
[ui] username=sakae editor=emacs
まず、上記のような .hgrcを$HOME直下に作る。editorはお好みで。 最近vimもpatchを1000個近く重ねて、めでたくメジャーバージョンアップしたよ。 でも、おいらは、emacsなんだ。軽くvi系を使うなら、nviだね。fedoraでは、/etc/profile.d/vim.shで アリアスをvi=vimにしてるんだけど、ここをnviにしといたよ。そして、emacs上から、ansi-term を起動して、nviを使うのが、editor戦争を回避するこつになってます。ああ、余計な事だったね。
話を元に戻すと、名前は必須。これを 書いておかないと機能しません。そして、初めの儀式を執り行います。
[sakae@fedora ~]$ cd /home/sakae/src/retro-11.5/vm/complete/go/src [sakae@fedora src]$ hg init [sakae@fedora src]$ hg status ? main/main.go ? ngaro/core.go ? ngaro/dev.go ? ngaro/img.go ? ngaro/ngaro.go [sakae@fedora src]$ hg commit -A -m "Original" adding main/main.go adding ngaro/core.go adding ngaro/dev.go adding ngaro/img.go adding ngaro/ngaro.go [sakae@fedora src]$ hg status [sakae@fedora src]$ hg log changeset: 0:675e93155510 tag: tip user: sakae date: Wed Sep 04 16:04:35 2013 +0900 summary: Original
initして、.hgって言う倉庫を作り、倉庫入り対象ファイルを確認し、それから倉庫へ入れた。 後は、更新したらコミットだな。
statsを表示するルーチンは入れてないけど、遅くなる要因は埋め込んだよ。
[sakae@fedora src]$ hg diff -r0 diff -r 675e93155510 ngaro/core.go --- a/ngaro/core.go Wed Sep 04 16:04:35 2013 +0900 +++ b/ngaro/core.go Thu Sep 05 13:52:46 2013 +0900 @@ -38,8 +38,12 @@ In Out Wait + NUM_OPS // DUMMY must be here ) +var stats[NUM_OPS + 1]int32 +var max_sp, max_rsp int + func (vm *VM) core(ip int32) (err error) { var port [nports]int32 var sp, rsp int @@ -51,15 +55,26 @@ } }() for ; int(ip) < len(vm.img); ip++ { + if (vm.img[ip] > NUM_OPS){ + stats[NUM_OPS]++ + } else { + stats[vm.img[ip]]++ + } switch vm.img[ip] { case Nop: case Lit: sp++ ip++ data[sp] = vm.img[ip] + if (max_sp < sp) { + max_sp = sp + } case Dup: :
で、遅い事が予想されるコードを実行してみるt
[sakae@fedora benchmarks]$ time ../statmain real 0m3.226s user 0m3.148s sys 0m0.046s
golangなんて言う若者も荷物を背負えばやっぱり遅いじゃん。この世の中にマジック なんて無い事が分かってほっとしましたよ。
今回初めて、goのコードを書いてみたけど、K&Rスタイルを強制させる文法は、 LinuxやGNUの行数水増しスタイルを矯正させる年寄りの頑固さを連想したよ。
go tool dist env
これ、goの自動化ベンチスクリプト中で使われていた指令です。一体これは何を するものぞ? manが無くて、代わりに go helpで参照していくと、
[sakae@fedora ~]$ go tool dist -h abort: there is no Mercurial repository here (.hg not found) go tool dist: FAILED: hg identify -b
ああ、これと同じようなエラーが出てたわい。.hgが有る所で実行してみるか。
[sakae@fedora ~]$ cd src/retro-11.5/vm/complete/go/src [sakae@fedora src]$ go tool dist -h abort: there is no Mercurial repository here (.hg not found) go tool dist: FAILED: hg identify -b [sakae@fedora src]$ ls -a . .. .hg main ngaro [sakae@fedora src]$ hg identify -b default
ちゃんとした所に移動してやってみてもgoはエラー。hgはちゃんと動いている。 という事は、goのソース嫁って事かな。この際なので、 ソースからのGo言語のインストールに則り、自前の go環境を整えてみる。gccなんかに比べると圧倒的な速さで、整った。
ALL TESTS PASSED --- Installed Go for linux/386 in /home/sakae/src/go Installed commands in /home/sakae/src/go/bin *** You need to add /home/sakae/src/go/bin to your PATH.
PATHを通して、エラーになってたgo tool dist env をやってみると、まだエラー。 ふと上のURLのオプション設定をみて
[sakae@fedora src]$ export GOROOT=/home/sakae/src/go [sakae@fedora src]$ go tool dist env GOROOT="/home/sakae/src/go" GOBIN="/home/sakae/src/go/bin" GOARCH="386" GOOS="linux" GOHOSTARCH="386" GOHOSTOS="linux" GOTOOLDIR="/home/sakae/src/go/pkg/tool/linux_386" GOCHAR="8" GO386="sse2"
GOROOTを設定してみたら、動き出した。環境問題だった訳ね。副作用でversionが 微妙に上がってた。ラッキーだな。
yumで入れたgoに対して、GOROOTを設定してみたけど、エラーが相変わらず発生する。 こりゃ、赤帽さんとこに問い合わせかな。お金取られるんだろうか?
.hgignore
上で初めてhgを使った。作業dirをsrcにしたけど、もう一段階上の方が良かったな。 そうすると、コンパイルする度にbin、pkgの下が更新されちゃうんで、これらを無視 したい。作業dirに .hgignoreを書いておけばいいんだな。
hg help ignoreで調べられるんだけど、例が出てた。
syntax: glob *.pyc *.orig *.rej *~ *.log* bin/* pkg/* tmp/ *.tmp *.TMP syntax: regexp \#.*\#$
おまけ
retroのgoバージョンに、statsの表示ルーチンを追加した。ライブラリィー内のfuncは 大文字で始めないいけないなんて、落とし穴だったなあ。Cと微妙に違ってて戸惑う事しきり。 A Tour of GOが、手っ取り速く巡れて便利だったよ。
--- a/src/main/main.go Thu Sep 05 16:22:00 2013 +0900 +++ b/src/main/main.go Fri Sep 06 16:35:35 2013 +0900 @@ -103,6 +103,7 @@ term := ngaro.NewTerm(clr, dim, input, os.Stdout) vm := ngaro.New(img, *dump, *shrink, term) err = vm.Run() + ngaro.RxStat() if *tty { // Unset raw mode exec.Command("/bin/stty", "-F", "/dev/tty", "echo", "cooked").Run() } diff -r 931c84da25ad -r 06727f582465 src/ngaro/core.go --- a/src/ngaro/core.go Thu Sep 05 16:22:00 2013 +0900 +++ b/src/ngaro/core.go Fri Sep 06 16:35:35 2013 +0900 @@ -192,3 +192,21 @@ } return } + +func RxStat() { + + var i int32 = 0 + + fmt.Printf("LIT: %d\n", stats[Lit]) + fmt.Printf("DUP: %d\n", stats[Dup]) + fmt.Printf("DROP: %d\n", stats[Drop]) +// + fmt.Printf("CALL: %d\n", stats[NUM_OPS]) + fmt.Printf("Max SP: %d\n", max_sp) + fmt.Printf("Max RSP: %d\n", max_rsp) + + for s := 0; s < NUM_OPS; s++ { + i += stats[s] + } + fmt.Printf("Total opcodes processed: %d\n", i) +}
どうも合計が合わないなあ。pkgの中で宣言した配列は自動で初期化されないのだろうか? 手抜きして、中抜きで表示してるんで、確認できんな。いつか調べてみよう。