Ruby 用の JIT コンパイラを作りました。ただし VM のではない。
このところ Ruby VM 用の JIT コンパイラが話題だったようですが、Ruby 用の JIT コンパイラを試作してみました。 ただし VM 用の JIT コンパイラではありません。 代わりに? OpenCL も利用できます(ただし macOS のみ)。
Ruby は埋め込み DSL (embedded DSL, eDSL) のホスト言語としてよく使われます。 今回試作した JIT コンパイラは、Ruby 向けに作った C 言語風の eDSL 用の JIT コンパイラです。 そういう DSL は、構文だけホスト言語である Ruby の構文を借りて、実行は自前の JIT コンパイラを使うので、我々はそのような DSL を 半パラサイト DSL (hemi-parasitic DSL) と呼んでいます。
なんのためのもの?
この eDSL は、Ruby プログラムの中で、一部の計算処理を高速実行したいときに使う eDSL です。 C 言語で関数を書き、それを ruby-ffi で呼び出してもよいのですが、そのあたりの作業を簡単にすることを目的としています。
もちろん Ruby VM の JIT コンパイラを開発して高速化することも大切なのですが、Ruby のセマンティクスを厳密に守りつつ高速実行するのは大変なはずで、ちょっと JIT コンパイラを書けばよいというものではありません。ですから、Ruby のセマンティクスはあまり守らずに、都合の悪いところは無視して高速なコードを生成する、というのは実用上は意味のあることかと思います。 なお今回の eDSL の場合、見た目は Ruby ですがセマンティクスはかなり異なります。 Ruby のセマンティックスはあまり守らず、というようより、全く異なる言語と思ってください。
例
簡単な例として Fibonacci 数を計算するプログラムを示します。
require 'yadriggy/c'
include Yadriggy::C::CType
def fib(n) ! Integer
typedecl n: Integer
if n > 1
return fib(n - 1) + fib(n - 2)
else
return 1
end
end
puts Yadriggy::C.run { return fib(32) }
fib
メソッドが eDSL で書かれた部分です。
C 言語風なので静的に型付けされます(Ruby でないので!)。
def
の行の !
のオペランドが戻り値の型で、typedecl
で引数 n
の型を宣言しています。
!
ではなく -
にした方が格好がよかったかもしれませんが、Ruby のメソッドとしても実行できるようにと思って !
を選びました。
上の例ではでてきませんが、局所変数がでてくる場合は、これも typedecl で型を宣言します(現仕様では、局所変数がある場合は引数の型宣言のための typedecl
とは別の行に typedecl
を書きます)。
ただし簡単な型推論はしますから、局所変数の型はしばしば省略可能です。
最後の行の run
メソッドでブロック
{ return fib(32) }
を渡しています。本 eDSL は Yadriggy (宿り木、です)というライブラリを使って実装しているのですが、このライブラリは、ブロックのソースコードを見つけてきて読み込み、抽象構文木を作ります。
さらにこのブロックの中から呼ばれている fib
も eDSL の一部と認識して、その抽象構文木も作ります。
eDSL の JIT コンパイラは、この抽象構文木から C 言語のソースを生成し、コンパイルして、得られたバイナリを ruby-ffi を使ってリンク、呼び出します。
fib(32)
の実行結果は return
によって Ruby 側に戻され、表示されます。
この eDSL は C 言語風なので、全ての戻り値は return
文で戻さなければならないことに注意してください。
この他の例は Wiki をごらんください。 配列を使った例や OpenCL を用いた例についての説明もあります。
なんで Ruby を選んだの?
最後に、なんで Ruby を選んだの? と聞かれることがありますが、それは Ruby の構文は(変態的^H^H^H非常に)柔軟で、かなりの範囲の構文を表現できるからです。
Yadriggy を作る前に Java で同様のものを作ったのですが、Java 言語は構文の自由度が小さく、また静的に型付けされるので、あまり Java とは異なる DSL を埋め込んでいる、という風になりませんでした。