GCCの勉強とmruby
最近ちょとmrubyのコードを見たりしていた。ついでにGCCの使い方を勉強しつつ、吐き出すアセンブラを眺めてみたり、そんな感じ。
勉強がてらVMまわりを少しいじってみたので自分用メモを残しておく。
眺めてたのはVMのコードで、mrubyのVMはGCCではダイレクトスレッデッドコードになるので、ラベルの配列を作ってgotoすることで命令を実行していく。
gotoするときはNEXTというマクロを使う。
#define NEXT mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)]
ここで疑問なのはmrb->arena_idxで、こいつは何に使うのかというと、ここに説明が書いてある。
http://www.dzeta.jp/~junjis/code_reading/index.php?mruby%2FGC%BD%E8%CD%FD%A4%F2%C6%C9%A4%E0
ようするにオブジェクトを大量に生成するときに一時的にGC用にいれておく場所???
ということであれば、mrb->arena_idxはオブジェクトを生成するとき以外は戻す必要は無いんじゃないか。
ところでVM命令のひとつ、OP_MOVEのコードはこうなっている。コメントアウトされているところは削った。
CASE(OP_MOVE) { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; NEXT; }
値のコピーしかしていないので、この場合はmrb->arena_idx=aiは無くてもいい(と思う)。
vm.cをGCC -O3 -Sでコンパイルしてアセンブラを出力してみる。OP_MOVEはoptableの2番目なのでそれを頼りにラベルを検索して、出てきたコードはJMPとかで飛びまくってるからそれらを繋げたものが以下。
L24: movl %ebx, %eax shrl $23, %eax sall $4, %eax addl -768(%ebp), %eax shrl $10, %ebx andl $8176, %ebx movl -768(%ebp), %edx addl %ebx, %edx movb 8(%edx), %cl movb %cl, 8(%eax) movl 4(%edx), %ecx movl (%edx), %edx movl %edx, (%eax) movl %ecx, 4(%eax) movl -780(%ebp), %ebx movl 12(%ebp), %ecx movl %ebx, 4236(%ecx) addl $4, -764(%ebp) movl -764(%ebp), %esi movl (%esi), %ebx movl %ebx, %eax andl $127, %eax movl _optable.3842(,%eax,4), %eax jmp *%eax
mrb->arena_idx=aiをしているのは真ん中ちょい下のmovl -780(%ebp), %ebxから3命令である。OP_MOVEは24命令なので、無駄な処理が1/8も存在することになる。
Cのコードを修正する。
CASE(OP_MOVE) { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; // NEXT; i=*++pc; goto *optable[GET_OPCODE(i)]; }
L24: movl %ebx, %eax shrl $23, %eax sall $4, %eax addl -768(%ebp), %eax shrl $10, %ebx andl $8176, %ebx movl -768(%ebp), %edx addl %ebx, %edx movb 8(%edx), %cl movb %cl, 8(%eax) movl 4(%edx), %ecx movl (%edx), %edx movl %edx, (%eax) movl %ecx, 4(%eax) addl $4, -764(%ebp) movl -764(%ebp), %ecx movl (%ecx), %ebx movl %ebx, %eax andl $127, %eax movl _optable.3842(,%eax,4), %eax jmp *%eax
3命令が消えた。それにともないレジスタesiが使われなくなった。この例では単純に使わないが、もっと入り組んだ命令ではこの効率アップの効果は大きくなるだろう。
ベンチ用コード
from = Time.now 10000000.times do a = 1 b = 2 c = 3 a = b b = c c = a end to = Time.now p to - from
OP_MOVEを並べるためだけに無駄な処理をしてみた。
んで、mruby --verboseして出てきた実行コード(timesのブロック内)
irep 149 nregs=6 nlocals=5 pools=0 syms=0 000 OP_LOADI R1 1 001 OP_LOADI R2 2 002 OP_LOADI R3 3 003 OP_MOVE R5 R2 004 OP_MOVE R1 R5 005 OP_MOVE R5 R3 006 OP_MOVE R2 R5 007 OP_MOVE R5 R1 008 OP_MOVE R3 R5 009 OP_RETURN R5
ローカル変数の転送にR5を経由するという激しく無駄なコードが出力されている。コードジェネレータには改善の余地がありそうだ。
んでベンチ。mruby_testのほうが修正したやつ。
D:\test>mruby test.rb 3.765625 D:\test>mruby_test test.rb 3.3125
1割以上速くなっているように見える。
まあ、mrb->arena_idx=aiがよくわかっていないのでバグるかもしれないのだけれども。