Digamma - Tracing JIT Compiler for Ypsilon

ネイティブコード生成についていろいろと考えていましたが、Tracing JITと呼ばれるものを試してみることにしました。

Tracing JITでポイントとなるのはインタープリタコンパイル済みのコードの切り替えに起因するオーバヘッドかと思います。またYpsilonではConcurrent GCの性能を落としてはいけないという制約がありますのでなかなか難しいです。

でも、面白いことにConcurrent GCとTraicing JITが上手くかみ合うとネイティブコンパイラの性能を越えるケースも出てくるようです。

まず、Ikarusより速くなるという特別なケースを紹介しましょう。

(define map-1
  (lambda (proc lst)
    (if (null? lst)
        '()
        (cons (proc (car lst))
              (map-1 proc (cdr lst))))))

(define minus (lambda (x) (- x)))
(define lst (make-list 1000000 1))
(define sink #f)
(time (set! sink (map-1 minus lst)))

結果は・・・

Linux ubuntu-core2 2.6.27-11-generic #1 SMP Thu Jan 29 19:24:39 UTC 2009 i686 GNU/Linux
   
Ikarus
  5 collections
  269 ms elapsed cpu time, including 168 ms collecting
  273 ms elapsed real time, including 168 ms collecting
  20558328 bytes allocated

Ypsilon 0.9.6-trunk/r417
  0.303697 real    0.536033 user    0.000000 sys

Ypsilon 0.9.6-trunk/r417 + Digamma(Tracing JIT)
  0.211938 real    0.416026 user    0.000000 sys (*1)

これはmap-1だけをコンパイルしてminusはインタープリタで実行した場合の結果です。minusが1000000回ほど実行されることを考えれば、何かを間違えていると思えるような結果ですね :D(念のため繰り返しますが、これはあくまで特別なケースですよ!)

map-1のASTは
http://codepad.org/DJjuXVd6
Digammaで生成したコードはこんな感じです(ライブラリ部分は省略しています)
http://codepad.org/kWQQ7u2z

map-1ではメモリアロケーションGCの影響が大きいので、こんどはfor-each-1を使ってコンパイル済みコードの特性を見てみることにします。

(define for-each-1
  (lambda (proc lst)
    (if (null? lst)
        (unspecified)
        (begin
          (proc (car lst))
          (for-each-1 proc (cdr lst))))))

(define lst (make-list 1000000 1))
(time (for-each-1 - lst)) ; *Ikarusでは(define unspecified void)が必要

結果は・・・

Linux ubuntu-core2 2.6.27-11-generic #1 SMP Thu Jan 29 19:24:39 UTC 2009 i686 GNU/Linux

Ikarus
  no collections
  24 ms elapsed cpu time, including 0 ms collecting
  23 ms elapsed real time, including 0 ms collecting
  0 bytes allocated

Ypsilon 0.9.6-trunk/r417
  0.075264 real    0.076004 user    0.000000 sys

Ypsilon 0.9.6-trunk/r417 + Digamma(Tracing JIT)
  0.037176 real    0.040003 user    0.000000 sys

さすがに、これでIkarusに勝とうというのは甘いですね :p
でもなかなかいい仕事しています :)

for-each-1のASTは
http://codepad.org/FZMo8jOV
生成されるコードは正しく末尾再帰なものになります。
http://codepad.org/5nu6nNNh
ちなみに、Digammaのプロトタイプは500行ほどのSchemeのプログラムなので、コードの品質はまだそれなりです :p
最後にコンパイルしたmap-1とfor-each-1がcall/cc対応になっているかどうかの確認です。

> (define cont #f)
> (define foo
    (lambda (n)
      (if (= n 222)
          (call/cc (lambda (k) (set! cont k) (- n)))
          (- n))))
> (map-1 foo '(111 222 333))
(-111 -222 -333)
> (cont 'ok?)
(-111 ok? -333)

> (define foo
    (lambda (n)
      (if (= n 222) (call/cc (lambda (k) (set! cont k))))
      (display n)
      (newline)))
> (for-each-1 foo '(111 222 333))
111
222
333
> (cont)
222
333
>

大丈夫なようです ^^b

さて、この記事だけを見ると明日にでもリリースできそうに見えるかと思いますが・・・
実は現在のDigammaはインタープリタコンパイルしたGCCのバージョンに強く依存しています。この依存を無くしてDigammaを実用化するにはインタープリタの実行コードを自動生成する仕組みが必要だと思っています。ですが、これに関して良いアイディアがまだ出ません orz。まあアセンブラで書いちゃうという最終手段はあるんですけどね :(

ちなみにDigammaの名前はFujitaのFが由来です :D
http://en.wikipedia.org/wiki/Digamma