あなとみー おぶ mrubyのJIT (その2)

前回の1.10001・・・は最初に証明された超越数の10倍でした。
私が積読してあることで名高い「人月の神話」には、つぎのようなくだりがあります。

私にフローチャートを見せられて、テーブルを見せないとしたら、私はずっと煙に巻かれたままになるだろう。逆にテーブルが見せてもらえるなら、フローチャートは大抵必要なくなる

そういうことで、mrubyのJITの解説第二回目はデータ構造です。

よく言われることですが、mrubyには大域変数が無く代わりにstate構造体(mrb_state)を定義してそのポインタをほぼすべての関数に引数として渡すようにしています。このような構造はVMを複数作れるとか嬉しいことが多いのですが、mrubyのJITに関しても恩恵にあずかっています。この恩恵については後で説明しますが(伏線回収できるかな?どきどき)、mrb_stateにちゃっかりJITで使う変数も忍ばせています。

  mrbjit_comp_info compile_info; /* JIT stuff */

なんと、忍ばせているのは1つだけ。なかなか謙虚ですね。ただ、これは構造体でこの中にいっぱい(といっても現状はそれほどでもない)メンバーがいます。

typedef void * mrbjit_code_area;
typedef struct mrbjit_comp_info {
  mrb_code *prev_pc;
  mrbjit_code_area code_base;
  int disable_jit;
  int nest_level;
} mrbjit_comp_info;

それぞれ説明します。ちなみにこの2つの構造体はinclude/mruby.hにあります。

  • prev_pc これはいまコンパイルしようとしているVMの命令の前に実行した命令のポインタが入っています。なんでこんなのが必要かというと、逐次実行で実行した場合とジャンプで飛んできた場合ではレジスタの設定状況とかが違う可能性が高いので別の命令列として扱うからなのですが、詳しくはコンパイルの処理のところで説明します(伏線広げすぎて大丈夫かなーどきどき)。
  • code_base これはXbyakのオブジェクト(MRBJitCode)へのポインタです。ここが管理するC++のオブジェクトが管理する記憶領域にコンパイルされたコードが格納されます。mrubyはC++ではなくCで書かれているのでmrubyを出来る限り変えないためにmruby側からはvoidへのポインタとなっています。C++側(jitcode.cc)でキャストして使います。
  • disable_jit これはコンパイルするかしないかを制御するフラグでnon-0だとコンパイルしません。なんでこんなのがいるかというと、mrb_runが再帰的に呼び出されると想定しているスタックレイアウトが狂ってバグるからです。再帰的に呼び出されるのはほとんどが短くて時間のかからない処理だと思うので多分これで問題ないです。
  • nest_level これは現在コンパイル中のメソッドのレベルを表します。Tracing JITだとメソッドのsendやenterがコンパイルされなくてreturn時だけコンパイルするような場合もあり得ますのでそういう場合にreturnをコンパイルしないようにするためのものです。

こんな感じです。思ったより書くのに時間がかかって全部書ききれなかったのですが、用事があるのでまたあとで続きを書きます。
用事が終わったので続きます。ビール美味しかった。

次に、irep関連の構造体を見てみます。mruby全体の情報はmrb_statusにあるわけですが、ここのメソッドの情報、例えば命令列(iseq)や定数テーブル(pool)はmrb_irepという
構造体に格納されます。mrb_irepの配列がmrb_statusのメンバーになっています。

mrb_status の一部です。

  struct mrb_irep **irep;
  size_t irep_len, irep_capa;

で、この命令列mrb_irepにもコンパイラで使う情報が忍ばせてあります。このmrb_irep他の定義はinclude/mruby/irep.hにあります。

  /* JIT stuff */
  int *prof_info;
  mrbjit_codetab *jit_entry_tab;

この二つのメンバーを説明します。

  • prof_info 命令ごとの実行回数を表します。ポインタなのは命令列の長さだけ配列を確保して使うからです。1000を超えるとコンパイルしてネイティブコードが実行されますので1002くらいまでしか更新されません。プロファイラの代わりにはならないわけです。カバレージなら取れそうです。
  • jit_entry_tab 命令ごとにネイティブコードのアドレスを表します。実はそれだけではなくCPUレジスタの使用状況とかXbyakで使うコード領域を複数持つためのフィールドとか持っているのですがまだ未使用です。

ここまでirep.hで定義されています。

jit_entry_tabの構造体mrbjit_codetabは、include/mruby/jit.hに定義されていて次のような構造をしています。

typedef struct mrbjit_codetab {
  int size;
  mrbjit_code_info *body;
} mrbjit_codetab;

sizeとあるので複数あることが分かります。ネイティブコードのアドレスなどの情報(code_info)は1つのVM命令に一つではなく複数あり得ます。なぜかと言えば、直前に実行した命令によってレジスタなどの内容が変わるため(前にも説明しましが)、生成されるコードが違う可能性があるからです。現状では直前に実行した命令と今実行中のメソッドの呼び出し元のアドレスで別々のcode_infoを割り当てています。ただし、呼び出し元のアドレスで別々のcode_infoになるのはメソッドをインライン展開する場合だけです。Tracing JITは簡単にインライン展開出来るのですが、やりすぎるとネイティブコードが膨れて却って遅くなるので特定の条件(現在は5命令以下のメソッド)だけで行っています。

code_infoの内容を示します。

typedef struct mrbjit_code_info {
  mrbjit_code_area code_base;
  mrb_code *prev_pc;
  mrb_code *caller_pc;
  void *(*entry)();
  mrbjit_varinfo dstinfo;	/* For Local assignment */
  mrbjit_inst_spec_info inst_spec;
  int used;
} mrbjit_code_info;

メンバーがたくさんありますが、現状余り使っていないです。将来最適化とかする時はフル活用して、メンバーも大幅に増えることでしょう。

使っているメンバーを紹介します。

  • prev_pc 前に実行した命令のアドレスです。code_info検索用
  • caller_pc 現在実行中のメソッドの呼び出し元のアドレスです。code_info検索用
  • entry ネイティブコードのエントリーアドレスです。説明するスタックフレームを整えたりした後、このアドレスをcallするとVMと同じ動きをします。
  • used このcode_infoが使われているかどうか。mallocの回数を節約するためcode_infoはまとめてアロケーションします。そのため、使っているかのフラグが必要です。

使っている構造体はこんな感じです。最適化とか始めると構造体の数の桁が変わることでしょう。早くそうなるといいですね(他人事)、ということで今回は終わり。

たぶん、次回もあることでしょう。予定は未定ですが。