技術

4.4. JITコンパイラの追加

LLVMによるプログラミング言語の実装チュートリアル日本語訳
第4章 万華鏡: JITの追加と最適化のサポート
第4節 JITコンパイラの追加

LLVM IRで得られるコードに対して適用可能なツールにはいろんな種類のものがある。
例えば、(前節でやったように)LLVM IRのコードに対して最適化を実行することも可能だし、それをテキスト形式またはバイナリでダンプして出力することも出来るし、色んなターゲットに対してそのコードをコンパイルしてアセンブリファイル(.s)を得る事もできるし、JITコンパイルをすることも出来る。
LLVM IR表現のいいところは、それがコンパイラにおける多くのパーツ間における”共通言語”である点である。

この節では、我々のインタプリタに対してJITコンパイラのサポートを追加する。
それを万華鏡に追加するために、ユーザーが関数を入力し終えたらJITコンパイルを実行する、ということを基本的なアイデアとする。
ただし、トップレベルの式は入力されたらただちに評価するようにする。
例えば、”1+2;”と入力されたら、それを評価して3と印字しなければならない。
もし関数が定義されたら、コマンドラインからそれを呼べるようにすべきである。

これを行うため、まずはJITを宣言して初期化する。
これをグローバル変数とmain関数における関数呼び出しの追加によって行う。

static ExecutionEngine *TheExecutionEngine;
...
int main() {
  ..
  // JITの生成。Moduleの所有権をとる。
  TheExecutionEngine = EngineBuilder(TheModule).create();
  ..
}

これによって、JITコンパイラにもLLVMインタプリタにもなれる、抽象的な”実行エンジン(Execution Engine)”が生成される。
あなたのプラットフォームにおいて利用可能なJITコンパイラがあれば、LLVMは自動的にそれを選択するし、そうでなければインタープリタへフォールバックする。

ExecutionEngineが生成されたら、JITを利用する準備は完了である。
たくさんの便利なAPIがそれには存在するが、一番シンプルなのは”getPointerToFunction(F)”メソッドである。
このメソッドは、与えられたLLVM関数に対してJITコンパイルを行い、生成された機械語への関数ポインタを返す。
我々の場合、これはトップレベルの式を構文解析するコードを以下のように変更しなければならないということを意味する。

static void HandleTopLevelExpression() {
  // トップレベルの式を評価し、無名関数にする。
  if (FunctionAST *F = ParseTopLevelExpr()) {
    if (Function *LF = F->Codegen()) {
      LF->dump();  // 説明目的として関数をダンプする。

      // 関数をJITコンパイルし、関数ポインタを返す。
      void *FPtr = TheExecutionEngine->getPointerToFunction(LF);

      // 関数ポインタを正しい型(引数なし、 doubleを返す)へ変換する。
      // なのでこの関数はネイティブの関数として呼び出すことが可能。
      double (*FP)() = (double (*)())(intptr_t)FPtr;
      fprintf(stderr, "Evaluated to %f\n", FP());
    }

トップレベルの式を、”引数なしでdoubleを返すLLVM関数”へコンパイルするようにした事を思い出して欲しい。
LLVMのJITコンパイラは、ネイティブなプラットフォームABI(Application Binary Interface)に適合するので、getPointerToFunctionで得られたポインタを関数ポインタにキャストでき、直接呼び出すことが出来る。
さらにそれは、JITコンパイルされたコードと、アプリケーションに静的リンクされたネイティブな機械語の間に違いがないということも意味する。

以上のたった2つの変更によって、万華鏡がどう動くようになったかを見てみよう。

ready> 4+5;
Read top-level expression:
define double @0() {
entry:
  ret double 9.000000e+00
}

Evaluated to 9.000000

基本的にはちゃんと動いてるようである。
関数のダンプは、入力されたトップレベルの式が総合され、”引数がなく常にdoubleの値を返す関数”となったという事を示している。
このデモは、非常に基本的な機能についてのものである。
しかしもっと複雑な例ではどうだろう。

ready> def testfunc(x y) x + y*2;
Read function definition:
define double @testfunc(double %x, double %y) {
entry:
  %multmp = fmul double %y, 2.000000e+00
  %addtmp = fadd double %multmp, %x
  ret double %addtmp
}

ready> testfunc(4, 10);
Read top-level expression:
define double @1() {
entry:
  %calltmp = call double @testfunc(double 4.000000e+00, double 1.000000e+01)
  ret double %calltmp
}

Evaluated to 24.000000

これは、ユーザ定義関数を呼び出すところを示しているが、ちょっと分かりにくい点がある。
testfuncの呼び出しを行う無名関数(トップレベルの式)についてのみJITの呼び出しを行っていて、testfuncそのものではJITの呼び出しは行われていないことに注意。
JITは、匿名関数から呼び出されている全ての未JITの関数をスキャンし、そしてgetPointerToFunction()から戻る前に未JITの関数全てをコンパイルして返す、ということが実際には行われている。

JITには、割り当て済みの機械語の解放や、関数の更新に合わせて再JITするような、より進んだインターフェイスがいくつかある。
しかしながら、このサンプルコードでは、そのようなものを使わなくても驚くべきパワフルな能力を得ている。
以下を確認してみよう。(私は匿名関数についてのダンプを出力するコードを取り除いた。今のあなたなら、そうするためにどうすればいいかは分かるだろう。)
訳注: この時点では、匿名関数についてダンプを出力するコード取り除かれていない。(なので以下の結果にもダンプは残ったまま。)
多分、ちょっとした演習として”各自、匿名関数についてダンプを出力するコードを取り除いてみよう!”と言ってるのだと思う。
ちなみに、この章の最後に全コードが載ってるが、それは取り除いた後のバージョンになっている。

ready> extern sin(x);
Read extern:
declare double @sin(double)

ready> extern cos(x);
Read extern:
declare double @cos(double)

ready> sin(1.0);
Read top-level expression:
define double @2() {
entry:
  ret double 0x3FEAED548F090CEE
}

Evaluated to 0.841471

ready> def foo(x) sin(x)*sin(x) + cos(x)*cos(x);
Read function definition:
define double @foo(double %x) {
entry:
  %calltmp = call double @sin(double %x)
  %multmp = fmul double %calltmp, %calltmp
  %calltmp2 = call double @cos(double %x)
  %multmp4 = fmul double %calltmp2, %calltmp2
  %addtmp = fadd double %multmp, %multmp4
  ret double %addtmp
}

ready> foo(4.0);
Read top-level expression:
define double @3() {
entry:
  %calltmp = call double @foo(double 4.000000e+00)
  ret double %calltmp
}

Evaluated to 1.000000

えぇ!?sinやcosについてJITはどうやって知ったの?
この答えは意外にも簡単である。
この例では、まずJITは関数の処理からはじめて、そして関数呼び出しを得る。
そしたらJITは、この関数はまだJITコンパイルされてないということに気づき、そしてその関数を解決するためのひとまとまりのルーチンの起動を開始する。
上の場合だと、関数本文が定義されてないので、JITは万華鏡のプロセスそれ自身の上で”dlsym(“sin”)”を呼んで終わる。
“sin”はJITのアドレス空間で定義されるので、JITはそのモジュールにおける呼び出しを、libmのsinを直接呼び出すよう取り繕う。

LLVMのJITは、未知の関数をどう解決するかについて制御するための多くのインターフェイスを提供する。(ExecutionEngine.hを参照)
それは、IRオブジェクトとアドレスの厳密な対応付けを規定すること(例えば静的なテーブルに対応付けたいときにLLVMのグローバル変数は便利である)もできるし、関数名によってオンザフライで動的に決定することも出来るし、関数が最初に呼ばれるまでJITのコンパイルを遅らせる事もできる。

その有用性の中でもひときわ面白いのは、命令を実装するために任意のC++コードによって言語を拡張できるという点である。
例えば、以下のようにしたとする。

/// putchard - double型の引数1つをうけとり0を返すputchar。
extern "C"
double putchard(double X) {
  putchar((char)X);
  return 0;
}

そしたら、”extern putchard(x); putchard(120);”のようにして、コンソールへ簡単な出力を生じさせることが出来る。
コンソールへは小文字のxが印字される。(120はアスキーコードにおける’x’を表す。)
似たようなコードは、ファイルI/Oや、標準入力からの入力や、万華鏡におけるその他諸々の機能を実装するのに使える。
訳注: この章の最後の全コードに、すでにputchardの定義が含まれているので、万華鏡をコンパイルして実行して、コンソールに”extern putchard(x); putchard(120);”と打ち込むだけで、上の例は再現出来る。

これにて、万華鏡のチュートリアルにおけるJITと最適化についての章は完了である。
これで、非チューリング完全なプログラミング言語を、ユーザーの使用法次第で、コンパイルしたり、最適化したり、JITコンパイルしたりすることが出来るようになった。
次の章では、いくつかの興味深いLLVM IRの問題点に立ち向かいながら、制御フロー構造による言語の拡張について見てみる。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示