パフォーマンスチューニング仕切り直し
前提
(let loop ([i 0]) (if (= i 10000000) '() (loop (+ i 1))))
このコードでは Mosh (530 msec)> Gauche (660 msec)。
ところがこのループに浮動小数の加算を入れると逆転。
(let loop ([i 0]) (if (= i 10000000) '() (begin (+ i 1.0) (loop (+ i 1)))))
仮説 Flonum の allocation が Mosh > Gauche かも?
どちらも Boehm GC を使っているので GC_PRINT_STATS 環境変数を有効にして allocd size で grep 。
命令列を比べてみよう
ループの一番実行される部分は Mosh の方が短い。
Gauche
0 CONSTI-PUSH(0) 1 LOCAL-ENV(1) 2 LREF0-PUSH ; i 3 CONST 10000000 5 BNUMNE 9 ; (= i 10000000) 7 CONSTN 8 RET 9 LREF0-PUSH ; i 10 CONST 1.0 12 NUMADD2 ; (+ i 1.0) 13 LREF0 ; i 14 NUMADDI(1) ; (+ i 1) 15 PUSH 16 LOCAL-ENV-JUMP(1) 2 ; (loop (+ i 1))
Mosh
CONSTANT_PUSH 0 ENTER 1 REFER_LOCAL_PUSH_CONSTANT_BRANCH_NOT_NUMBER_EQUAL 0 10000000 5 CONSTANT () LOCAL_JMP 15 REFER_LOCAL_PUSH_CONSTANT 0 1.000000 NUMBER_ADD REFER_LOCAL_PUSH_CONSTANT 0 1 NUMBER_ADD_PUSH SHIFTJ 1 1 -1 LOCAL_JMP -21
仮説2 浮動小数のときに Mosh の NUMBER_ADD が NUMADD2 より遅い
まずはざっくりと処理を追っていくのが良さそう。
Gauche
vminsn.c
label_NUMADD2: CASE(SCM_VM_NUMADD2) { #line 1088 "vminsn.scm" {ScmObj arg;POP_ARG(arg); if ((SCM_INTP(arg))&&(SCM_INTP(VAL0))) {long r=(SCM_INT_VALUE(arg))+(SCM_INT_VALUE(VAL0)); if (SCM_SMALL_INT_FITS(r)){VAL0=(SCM_MAKE_INT(r));NEXT1;} else {VAL0=(Scm_MakeInteger(r));NEXT1;}} else {VAL0=(Scm_Add(arg,VAL0));NEXT1;}}}
Scm_Add に行く。
ScmObj Scm_Add(ScmObj arg0, ScmObj arg1) { if (SCM_INTP(arg0)) { if (SCM_INTP(arg1)) { long r = SCM_INT_VALUE(arg0) + SCM_INT_VALUE(arg1); return Scm_MakeInteger(r); } if (SCM_BIGNUMP(arg1)) { if (SCM_EXACT_ZERO_P(arg0)) return arg1; return Scm_BignumAddSI(SCM_BIGNUM(arg1), SCM_INT_VALUE(arg0)); } if (SCM_RATNUMP(arg1)) { if (SCM_EXACT_ZERO_P(arg0)) return arg1; return Scm_RatnumAdd(arg0, arg1); } if (SCM_FLONUMP(arg1)) { // ここ if (SCM_EXACT_ZERO_P(arg0)) return arg1; return Scm_MakeFlonum((double)SCM_INT_VALUE(arg0) + SCM_FLONUM_VALUE(arg1)); }
Scm_MakeFlonum
ScmObj Scm_MakeFlonum(double d) { ScmFlonum *f = SCM_NEW(ScmFlonum); SCM_SET_CLASS(f, SCM_CLASS_REAL); f->value = d; return SCM_OBJ(f); }
Mosh
CASE(NUMBER_ADD) { const Object n = pop(); // short cut for Fixnum. Benmarks tell me this is strongly required. if (n.isFixnum() && ac_.isFixnum()) { const int32_t val = n.toFixnum() + ac_.toFixnum(); ac_ = Bignum::makeInteger(val); } else if (n.isNumber() && ac_.isNumber()) { ac_ = Arithmetic::add(n, ac_); } else { callWrongTypeOfArgumentViolationAfter(this, "+", "number", L2(n, ac_)); } NEXT1; }
ああ。予想がついたが先に進もう。
if (n1.isFixnum()) {\ if (n2.isFixnum()) {\ return Bignum::op(n1.toFixnum(), n2.toFixnum());\ } else if (n2.isRatnum()) {\ return Ratnum::op(n1.toFixnum(), n2.toRatnum());\ } else if (n2.isFlonum()) {\ return Flonum::op(n1.toFixnum(), n2.toFlonum());\
このあたりは Gauche と変わらない。
問題は明らかだ。コードを追ってやっと分かった。 isNumber が毎回走るのが遅い。
コード上は一見きれいに見えるのだけど、isNumber の中では isFixnum() || isFlonum() || などとやっているのでそりゃ遅いわ。
isNumber() を呼び出さないようにしてみたところ Gauche より速くなった。
isNumber() を呼び出さないのはまずいので処理を変えよう。
} else { ac_ = Arithmetic::add(n, ac_); if (ac_.isFalse()) { callWrongTypeOfArgumentViolationAfter(this, "+", "number", L2(n, ac_)); } }
これでもぎりぎり Mosh > Gauche なのでチューニング完了。ADD に限らず同じような事をやっている部分を修正。
解決。