SchemeからCへの実験
さて、いまイプシロンのドキュメントを書いているわけなのですが、そうすると何か新しいことを試したくなってきます。これは本能なのでしょうか :p
とういわけで、イプシロンのネイティブコード生成は1.0以降を予定しているのですが、今回ちょっとだけ実験してみました。fibで試してみます。
;; 末尾再帰でないfib (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))
((close (1 0 . fib) (<n.iloc (0 . 0) 2) (if.true (ret.iloc 0 . 0)) (call (push.n+.iloc (0 . 0) -1) (apply.gloc.of fib)) (push) (call (push.n+.iloc (0 . 0) -2) (apply.gloc.of fib)) (push) (ret.subr.gloc.of +)) (set.gloc.of fib) (ret.const.unspec))
これを手でCのSUBRの形式に書き直します。それぞれのインストラクションに対応する処理コードをvm1.cppから取ってきてほぼそのまま継げています。ただし自分自身への呼び出しは一般のSUBRへの呼び出しとして書いています。
scm_obj_t subr_cfib(VM* vm, int argc, scm_obj_t argv[]) { scm_obj_t stack[2]; // note: need gc protect scm_obj_t obj; if (argc == 1) { // (<n.iloc (0 . 0) 2) obj = argv[0]; if (FIXNUMP(obj)) { vm->m_value = ((intptr_t)obj < (intptr_t)MAKEFIXNUM(2)) ? scm_true : scm_false; } else if (real_pred(obj)) { vm->m_value = n_compare(vm->m_heap, obj, MAKEFIXNUM(2)) < 0 ? scm_true : scm_false; } else { goto bad_type_arg; } // (if.true (ret.iloc 0 . 0)) if (vm->m_value != scm_false) return argv[0]; // (call (push.n+.iloc (0 . 0) -1) (apply.gloc.of fib)) obj = argv[0]; if (FIXNUMP(obj)) { intptr_t n = FIXNUM(obj) - 1; if ((n <= FIXNUM_MAX) & (n >= FIXNUM_MIN)) obj = MAKEFIXNUM(n); else obj = intptr_to_integer(vm->m_heap, n); } else { if (number_pred(obj)) obj = arith_add(vm->m_heap, obj, MAKEFIXNUM(-1)); else goto bad_type_arg; } stack[0] = obj; obj = subr_cfib(vm, 1, stack); if (obj == scm_undef) return obj; // (push) stack[0] = obj; // (call (push.n+.iloc (0 . 0) -2) (apply.gloc.of fib)) obj = argv[0]; if (FIXNUMP(obj)) { intptr_t n = FIXNUM(obj) - 2; if ((n <= FIXNUM_MAX) & (n >= FIXNUM_MIN)) obj = MAKEFIXNUM(n); else obj = intptr_to_integer(vm->m_heap, n); } else { if (number_pred(obj)) obj = arith_add(vm->m_heap, obj, MAKEFIXNUM(-2)); else goto bad_type_arg; } stack[1] = obj; obj = subr_cfib(vm, 1, &stack[1]); if (obj == scm_undef) return obj; // (push) stack[1] = obj; // (ret.subr.gloc.of +)) if (FIXNUMP(stack[0]) && FIXNUMP(stack[1])) { intptr_t n = FIXNUM(stack[0]) + FIXNUM(stack[1]); if ((n <= FIXNUM_MAX) & (n >= FIXNUM_MIN)) return MAKEFIXNUM(n); else return intptr_to_integer(vm->m_heap, n); } else { if (number_pred(stack[0]) && number_pred(stack[1])) return arith_add(vm->m_heap, stack[0], stack[1]); goto bad_type_arg; } } wrong_number_of_arguments_violation(vm, "cfib", 0, 0, argc, argv); return scm_undef; bad_type_arg: wrong_type_argument_violation(vm, "cfib", 0, "real number", argv[0], argc, argv); return scm_undef; }
上のコードはタイプチェックとエラーチェックを含んだものです。型情報についてはなんの仮定もしていません。なのでこんな風に普通に動きます。
> (cfib 35) ;; 整数 9227465 > (cfib 35.0) ;; 浮動小数点数 9227465.0 > (cfib 1+9i) ;; 複素数はエラー error in cfib: expected real number, but got 1+9i
さて、最適化はgccの-O3頼みのこのコードでどのくらいの速度がでるのでしょう?
> (time (fib 39)) ;; 12.630728 real 12.62879 user 0.0 sys 63245986 > (time (cfib 39)) ;; 2.838955 real 2.840177 user 0.0 sys 63245986
約4.45倍になりました。
ちなみに以前GoogleV8を試したときの結果はこんな感じでした
Google V8 JavaScript EngineでFFIをテスト - Y.FUJITA::NOTEPAD::YPSILON
$ ypsilon v8drive.scm 63245986 ;; 2.264296 real 2.256141 user 0.0 sys
もちろん、こんな方法で何でも簡単にネイティブコードに変換できるほど甘くはないです。でも思ったよりも速かったので・・・う〜ん、いろいろと考えさせられますね。ちなみに"SchemeからCへのコンパイルにしようかな?"とか考えてるわけではありませんよ :p