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の中で宣言した配列は自動で初期化されないのだろうか? 手抜きして、中抜きで表示してるんで、確認できんな。いつか調べてみよう。