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