Kinx ライブラリ - JIT コンパイラ・ライブラリ
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。JIT コンパイルのためのライブラリを作ってみました。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
- Pull Request 等お待ちしております。
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
JIT やりたいよね。今回は Kinx - native でも使っている SLJIT を使いやすくしようということで、ライブラリ化しました。SLJIT 自体、ドキュメントが少なくてソースから解読して使っているので、SLJIT そのものの使い方を備忘録的に書こうかとも思ったのだけど、今回は保留。どこかでやるかもしれない。
しかし、SLJIT をそのまま使うよりも勿論使いやすくなっているので、こっちのほうが良いと思う。ホスト言語もスクリプト なので、手軽に楽しめるでしょう。
どんな感じ?
先にどんな感じのプログラムになるか、サンプルを出しておきます。色々細かい話が続いてここまでたどり着いてくれなさそうなので。。。
using Jit;
var c = new Jit.Compiler();
var entry1 = c.enter();
var jump0 = c.ge(Jit.S0, Jit.IMM(3));
c.ret(Jit.S0);
var l1 = c.label();
c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
c.call(entry1);
c.mov(Jit.S1, Jit.R0);
c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
c.call(entry1);
c.add(Jit.R0, Jit.R0, Jit.S1);
c.ret(Jit.R0);
jump0.setLabel(l1);
var code = c.generate();
for (var i = 1; i <= 42; ++i) {
var tmr = new SystemTimer();
var r = code.run(i);
System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}
Jit.Compiler
オブジェクトを作って、enter
で関数エントリを作り、色々レジスタをいじくりまわして ret
するコードを書きます。で、実行するときは generate()
して run()
、となります。generate()
して dump()
とすると、アセンブル・リストを見ることもできます。
色々とばしたい方は サンプル へ Go! → サンプルでは Ruby、Python、PyPy とのベンチマークもしてますよ。
SLJIT
そもそも SLJIT とは何か。
一言でいえば 抽象化アセンブラ で、一つの書き方で複数の環境をサポートできてしまうという、CPU ごとに異なるので作り直さなければならないというアセンブラの問題を解決してくれるライブラリです。現状サポートしているというプラットフォームは以下の通り。
- SLJIT のサポート・プラットフォーム
- Intel-x86 32
- AMD-x86 64
- ARM 32 (ARM-v5, ARM-v7 and Thumb2 instruction sets)
- ARM 64
- PowerPC 32
- PowerPC 64
- MIPS 32 (III, R1)
- MIPS 64 (III, R1)
- SPARC 32
ただし、ここで紹介する Kinx 版 JIT ライブラリは 64bit しかサポートしていないこと、および x64 Windows と x64 Linux しか確認して(できて)いませんので、そこは悪しからずご了承ください。
公式? 説明文書
私が知る限り、参考になる文書は以下程度しか見つかりませんでした。
- https://zherczeg.github.io/sljit/
- http://ftp.jaist.ac.jp/pub/NetBSD/NetBSD-current/src/sys/external/bsd/sljit/dist/doc/tutorial/sljit_tutorial.html
参考にはなります。
GitHub のリポジトリは以下です。
Jit
さて、Kinx ライブラリとしての JIT ライブラリ。C のまま使うより便利にはしてます。もちろん C のライブラリを使えばもっとより細かく制御できると思いますが、それなりのことはできます。
using Jit
Jit ライブラリは標準組み込みではないため、using ディレクティブを使用して明示的に読み込む。
using Jit;
Jit オブジェクト
Jit オブジェクトはパラメータ用のメソッドとコンパイラ・クラスが定義されている。
Jit パラメータ用メソッド
Jit のパラメータとして、即値、レジスタ、メモリアクセスの 3 種類がある。以下の形で利用する。
即値、メモリアクセス
即値、メモリアクセスは以下のメソッドで利用する。Jit.VAR()
はローカル変数領域を使うための特別なメソッド。スタック領域にローカル変数領域が自動的に確保され、その領域を使う。
メソッド | 備考 |
---|---|
Jit.IMM(v) |
64bit 整数、浮動小数点数どちらも同じ書き方。代入先のレジスタと合わせる。 |
Jit.VAR(n) |
ローカル変数領域。1 変数 8 バイト固定。 |
Jit.MEM0(address) |
address の示すアドレスのメモリ値を取得する。ただし現在実アドレスをスクリプトから指定できないので、スクリプトからは使えない。 |
Jit.MEM1(r1, offset) |
r1 に指定したレジスタをアドレスとみて、offset 位置(バイト単位)のメモリ値を取得する。 |
Jit.MEM2(r1, r2, shift) |
shift は 0 なら 1 バイト、1 なら 2 バイト、 2 なら 4 バイト、3 なら 8 バイトを示し、r1 + r2 * (shiftで示すバイト分) の位置のメモリ値を取得する。 |
レジスタ
以下のレジスタを使用可能。関数内で使えるレジスタの数は自動的に計算され、関数(enter()
で区切った範囲)ごとに変わる。
レジスタ | 用途 |
---|---|
Jit.R0 ~ Jit.R5
|
汎用レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。 |
Jit.S0 ~ Jit.S5
|
汎用レジスタ。別関数呼び出し後に破棄されない保証。 |
Jit.FR0 ~ Jit.FR5
|
浮動小数点レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。 |
Jit.FS0 ~ Jit.FS5
|
浮動小数点レジスタ。別関数呼び出し後に破棄されない保証。 |
尚、Floating Point 用のレジスタは FR
/FS
合わせて最大で 6 個までなので、FR4
まで使用した場合、FS0
しか使えません。FR5
まで使うと FS*
は全て使えません。以下のような感じですのでご注意を。
FR* レジスタ |
FS* レジスタ |
---|---|
(使えない) |
FS0 , FS1 , FS2 , FS3 , FS4 , FS5
|
FR0 |
FS0 , FS1 , FS2 , FS3 , FS4
|
FR0 , FR1
|
FS0 , FS1 , FS2 , FS3
|
FR0 , FR1 , FR2
|
FS0 , FS1 , FS2
|
FR0 , FR1 , FR2 , FR3
|
FS0 , FS1
|
FR0 , FR1 , FR2 , FR3 , FR4
|
FS0 |
FR0 , FR1 , FR2 , FR3 , FR4 , FR5
|
(使えない) |
Jit コンパイラ
Jit 命令を作っていくためには Jit コンパイラ・オブジェクトを作成する。
var c = new Jit.Compiler();
Jit コンパイラには以下のメソッドがある。
Jit コンパイラ・メソッド | 復帰値 | 概要 |
---|---|---|
Jit.Compiler#label() |
label | 現在の場所にラベルを付加する。 |
Jit.Compiler#makeConst(reg, init) |
ConstTarget | コード生成後に即値を設定するための仮定義コードを出力する。 |
Jit.Compiler#localp(dst, offset) |
ローカル変数の実アドレスを取得するコードを出力する。dst に示したレジスタに格納される。offset はローカル変数番号。 |
|
Jit.Compiler#enter(argType) |
label | 関数の入り口を作成。引数タイプを指定できる(省略可)。 |
Jit.Compiler#fastEnter(reg) |
label | 関数の入り口を作成。ただし、余計なエピローグ、プロローグは出力せず、復帰アドレスを reg に保存する。 |
Jit.Compiler#ret(val) |
Return コードを出力する。val を返す。val は浮動小数点数は FR0 レジスタ、それ以外は R0 レジスタで返却される。 |
|
Jit.Compiler#f2i(dst, op1) |
double を int64_t にキャストするコードを出力する。dst は汎用レジスタ。op1 は浮動小数点レジスタ。 |
|
Jit.Compiler#i2f(dst, op1) |
int64_t を double にキャストするコードを出力する。dst は浮動小数点レジスタ。op1 は汎用レジスタ。 |
|
Jit.Compiler#mov(dst, op1) |
dst に op1 を代入するコードを出力する。浮動小数点とそれ以外の型は自動的に認識する。 |
|
Jit.Compiler#neg(dst, op1) |
op1 の符号反転した結果を dst に格納するコードを出力する。 |
|
Jit.Compiler#clz(dst, op1) |
op1 の先頭から 0 であるビットの数を数え、dst に格納するコードを出力する。 |
|
Jit.Compiler#add(dst, op1, op2) |
op1 と op2 を加算した結果を dst に格納するコードを出力する。 |
|
Jit.Compiler#sub(dst, op1, op2) |
op1 と op2 を減算した結果を dst に格納するコードを出力する。 |
|
Jit.Compiler#mul(dst, op1, op2) |
op1 と op2 を乗算した結果を dst に格納するコードを出力する。 |
|
Jit.Compiler#div(dst, op1, op2) |
浮動小数点数のみ、op1 と op2 を除算した結果を dst に格納するコードを出力する。 |
|
Jit.Compiler#div() |
汎用レジスタで符号なしとして除算した値を R0 レジスタに格納するコードを出力する。 |
|
Jit.Compiler#sdiv() |
汎用レジスタで符号付きとして除算した値を R0 レジスタに格納するコードを出力する。 |
|
Jit.Compiler#divmod() |
汎用レジスタで符号なしとして除算した値を R0 レジスタに格納し、余りを R1 レジスタに格納するコードを出力する。 |
|
Jit.Compiler#sdivmod() |
汎用レジスタで符号付きとして除算した値を R0 レジスタに格納し、余りを R1 レジスタに格納するコードを出力する。 |
|
Jit.Compiler#not(dst, op1) |
op1 のビット反転した結果を dst に格納するコードを出力する。 |
|
Jit.Compiler#and(dst, op1, op2) |
op1 と op2 でビット AND した値を dst に格納するコードを出力する。 |
|
Jit.Compiler#or(dst, op1, op2) |
op1 と op2 でビット OR した値を dst に格納するコードを出力する。 |
|
Jit.Compiler#xor(dst, op1, op2) |
op1 と op2 でビット XOR した値を dst に格納するコードを出力する。 |
|
Jit.Compiler#shl(dst, op1, op2) |
op1 を op2 ビット分、左シフトした値を dst に格納するコードを出力する。 |
|
Jit.Compiler#lshr(dst, op1, op2) |
op1 を op2 ビット分、論理右シフトした値を dst に格納するコードを出力する。 |
|
Jit.Compiler#ashr(dst, op1, op2) |
op1 を op2 ビット分、算術右シフトした値を dst に格納するコードを出力する。 |
|
Jit.Compiler#call(label) |
JumpTarget |
enter() 定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。label を指定した場合は後から設定する必要はない。 |
Jit.Compiler#fastCall(label) |
JumpTarget |
fastEnter() で定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。 |
Jit.Compiler#jmp(label) |
JumpTarget |
jmp コマンドを出力する。label を指定した場合は後から設定する必要はない。 |
Jit.Compiler#ijmp(dst) |
JumpTarget |
jmp コマンドを出力する。dst はアドレスを示すレジスタ、または即値。 |
Jit.Compiler#eq(op1, op2) |
JumpTarget |
op1 == op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#neq(op1, op2) |
JumpTarget |
op1 != op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#lt(op1, op2) |
JumpTarget | 符号なしとして op1 < op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#le(op1, op2) |
JumpTarget | 符号なしとして op1 <= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#gt(op1, op2) |
JumpTarget | 符号なしとして op1 > op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#ge(op1, op2) |
JumpTarget | 符号なしとして op1 >= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#slt(op1, op2) |
JumpTarget | 符号付きとして op1 < op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#sle(op1, op2) |
JumpTarget | 符号付きとして op1 <= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#sgt(op1, op2) |
JumpTarget | 符号付きとして op1 > op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#sge(op1, op2) |
JumpTarget | 符号付きとして op1 >= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。 |
Jit.Compiler#generate() |
JitCode | コード生成を行う。 |
Jit.Compiler#enter(argType)
関数の入り口を enter
メソッドで定義するが、argType
を指定しなかった場合は Jit.ArgType.SW_SW_SW
を指定されたものとみなす。引数は 3 つまで(仕様)で、それぞれの型を指定する。
-
SW
... Signed Word (64bit) -
UW
... Unsigned Word (64bit) -
FP
... Floating Point (64bit)
実際問題としては受け取るレジスタのビット列が同じになるため SW
と UW
は変わらないが、もしかしたら将来何か違いを出すかもしれない。尚、最後の引数から SW
は省略可能。なので、以下は全部同じ意味となる。
Jit.ArgType.SW_SW_SW
Jit.ArgType.SW_SW
Jit.ArgType.SW
引数で渡されるレジスタは決まっており、以下のようになっている。
- 呼び出し側
型 | 第 1 引数 | 第 2 引数 | 第 3 引数 |
---|---|---|---|
Integer | Jit.R0 |
Jit.R1 |
Jit.R2 |
Double | Jit.FR0 |
Jit.FR1 |
Jit.FR2 |
- 受け取り側
型 | 第 1 引数 | 第 2 引数 | 第 3 引数 |
---|---|---|---|
Integer | Jit.S0 |
Jit.S1 |
Jit.S2 |
Double | Jit.FS0 |
Jit.FS1 |
Jit.FS2 |
呼び出し側でセットするレジスタと、受け取り側で受け取るレジスタが異なることに注意。
ConstTarget
ラベルアドレスを setLabel()
で設定する。
ラベルのアドレスを即値としてレジスタやメモリに格納したいときに使う。あまり使う機会は無いか。ジャンプテーブルの代わりになるかとも思うが、テーブルの作り方がイマイチうまい仕組みを用意できていないので。
ちなみに、即値を setValue()
で設定することもできるが、普通に Jit.IMM(100)
とか、浮動小数点数でも Jit.IMM(0.1)
とかできるようにしたので、こちらを使う意味はあまりない。
仮にジャンプテーブルに使うとした際の例は後述。
JumpTarget
ジャンプ先、もしくは関数コールのためのアドレスを setLabel()
で設定する。
例えば、比較した結果で分岐させる場合、以下のようになる。
var c = new Jit.Compiler();
// 関数のエントリポイント。
c.enter();
// S0レジスタ値 >= 3
var jump0 = c.ge(Jit.S0, Jit.IMM(3));
... // 条件が偽のときのコード
var jump1 = c.jmp();
var label0 = c.label();
... // 条件が真のときのコード
var label1 = c.label();
...
jump0.setLabel(label0);
jump1.setLabel(label1);
JitCode
generate()
メソッドでコード生成に成功すると、JitCode オブジェクト返る。JitCode オブジェクトのメソッドは以下の通り。尚、引数は 3 つまでしか指定できないことに注意(仕様)。抽象化アセンブラなので、様々なアーキテクチャに対応するために必要な仕様です。必要ならローカル変数領域を確保して、その先頭アドレスを渡すなどの工夫が必要。サンプルは後述。
メソッド | 概要 |
---|---|
JitCode#run(a1, a2, a3) |
復帰値を Integer として受け取る。 |
JitCode#frun(a1, a2, a3) |
復帰値を Double として受け取る。 |
JitCode#dump() |
ジェネレートされたアセンブル・リストを出力する。 |
サンプル
フィボナッチ数列(再帰版)
では、恒例のフィボナッチ数列を算出する再帰版のコードを書いてみましょう。サンプルとして最初に提示したものそのままです。
var c = new Jit.Compiler();
var entry1 = c.enter();
var jump0 = c.ge(Jit.S0, Jit.IMM(3));
c.ret(Jit.S0);
var l1 = c.label();
c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
c.call(entry1);
c.mov(Jit.S1, Jit.R0);
c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
c.call(entry1);
c.add(Jit.R0, Jit.R0, Jit.S1);
c.ret(Jit.R0);
jump0.setLabel(l1);
var code = c.generate();
for (var i = 1; i <= 42; ++i) {
var tmr = new SystemTimer();
var r = code.run(i);
System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}
結果は以下の通り。
[ 0.000] fib( 1) = 1
[ 0.000] fib( 2) = 2
[ 0.000] fib( 3) = 3
[ 0.000] fib( 4) = 5
[ 0.000] fib( 5) = 8
[ 0.000] fib( 6) = 13
[ 0.000] fib( 7) = 21
[ 0.000] fib( 8) = 34
[ 0.000] fib( 9) = 55
[ 0.000] fib(10) = 89
[ 0.000] fib(11) = 144
[ 0.000] fib(12) = 233
[ 0.000] fib(13) = 377
[ 0.000] fib(14) = 610
[ 0.000] fib(15) = 987
[ 0.000] fib(16) = 1597
[ 0.000] fib(17) = 2584
[ 0.000] fib(18) = 4181
[ 0.000] fib(19) = 6765
[ 0.000] fib(20) = 10946
[ 0.000] fib(21) = 17711
[ 0.000] fib(22) = 28657
[ 0.000] fib(23) = 46368
[ 0.000] fib(24) = 75025
[ 0.000] fib(25) = 121393
[ 0.001] fib(26) = 196418
[ 0.001] fib(27) = 317811
[ 0.001] fib(28) = 514229
[ 0.002] fib(29) = 832040
[ 0.002] fib(30) = 1346269
[ 0.004] fib(31) = 2178309
[ 0.006] fib(32) = 3524578
[ 0.009] fib(33) = 5702887
[ 0.016] fib(34) = 9227465
[ 0.035] fib(35) = 14930352
[ 0.042] fib(36) = 24157817
[ 0.066] fib(37) = 39088169
[ 0.119] fib(38) = 63245986
[ 0.181] fib(39) = 102334155
[ 0.289] fib(40) = 165580141
[ 0.476] fib(41) = 267914296
[ 0.773] fib(42) = 433494437
ちなみに、fib(42)
の結果を Ruby, Python, PyPy, PHP, HHVM, Kinx, Kinx(native) で計測して比較してみた。JIT ライブラリ版は上記は run()
の時間しか計ってないので、スクリプト解釈と JIT コード生成まで含め、全て公平にプロセス全体の user time で算出。
速い順に並べると以下の通り。やはり JIT で直接ネイティブ・コードを出力させると際立って速い。何気に Kinx(native) が PyPy より速かったのは嬉しい誤算。HHVM とどっこいくらい。スクリプトでは Ruby 速くなったなー。1.8 時代とか知ってると感慨深いですねー。
言語 | 版数 | User 時間 |
---|---|---|
Kinx(Jit-Lib) | 0.10.0 | 0.828 |
HHVM | 3.21.0 | 2.227 |
Kinx(native) | 0.10.0 | 2.250 |
PyPy | 5.10.0 | 3.313 |
PHP | 7.2.24 | 11.422 |
Ruby | 2.5.1p57 | 14.877 |
Kinx | 0.10.0 | 27.478 |
Python | 2.7.15+ | 41.125 |
尚、先の JIT ライブラリでジェネレートされたアセンブル・リストはこちら。Windows と Linux で違うのだが、今回は Linux。
0: 53 push rbx
1: 41 57 push r15
3: 41 56 push r14
5: 48 8b df mov rbx, rdi
8: 4c 8b fe mov r15, rsi
b: 4c 8b f2 mov r14, rdx
e: 48 83 ec 10 sub rsp, 0x10
12: 48 83 fb 03 cmp rbx, 0x3
16: 73 0d jae 0x25
18: 48 89 d8 mov rax, rbx
1b: 48 83 c4 10 add rsp, 0x10
1f: 41 5e pop r14
21: 41 5f pop r15
23: 5b pop rbx
24: c3 ret
25: 48 8d 43 fe lea rax, [rbx-0x2]
29: 48 89 fa mov rdx, rdi
2c: 48 89 c7 mov rdi, rax
2f: e8 cc ff ff ff call 0x0
34: 49 89 c7 mov r15, rax
37: 48 8d 43 ff lea rax, [rbx-0x1]
3b: 48 89 fa mov rdx, rdi
3e: 48 89 c7 mov rdi, rax
41: e8 ba ff ff ff call 0x0
46: 49 03 c7 add rax, r15
49: 48 83 c4 10 add rsp, 0x10
4d: 41 5e pop r14
4f: 41 5f pop r15
51: 5b pop rbx
52: c3 ret
Const の例
Const の例として、あえて書くならこんな感じ。ローカル変数にジャンプテーブルを作っているので、毎回テーブルを作り直していてイマイチ。テーブルだけ作成してアドレスを渡せるようなインターフェースを別途用意すれば解決しそうではある(やるかも)。
var c = new Jit.Compiler();
c.enter();
c.mov(Jit.R1, Jit.IMM(-1));
var jump0 = c.slt(Jit.S0, Jit.IMM(0));
var jump1 = c.sgt(Jit.S0, Jit.IMM(3));
var const0 = c.makeConst(Jit.VAR(0));
var const1 = c.makeConst(Jit.VAR(1));
var const2 = c.makeConst(Jit.VAR(2));
var const3 = c.makeConst(Jit.VAR(3));
// ローカル変数のアドレスを S0 レジスタ(第一引数)のオフセットで取得し R0 レジスタに格納。
c.localp(Jit.R0, Jit.S0);
// ローカル変数の値自体を取得。
c.mov(Jit.R0, Jit.MEM1(Jit.R0));
// ローカル変数の中身をアドレスと見立ててジャンプ。
c.ijmp(Jit.R0);
var l0 = c.label();
c.mov(Jit.R1, Jit.IMM(102));
c.ret(Jit.R1);
var l1 = c.label();
c.mov(Jit.R1, Jit.IMM(103));
c.ret(Jit.R1);
var l2 = c.label();
c.mov(Jit.R1, Jit.IMM(104));
c.ret(Jit.R1);
var l3 = c.label();
c.mov(Jit.R1, Jit.IMM(105));
var l4 = c.label();
c.ret(Jit.R1);
// ジャンプアドレスはコード生成前にセットする。
jump0.setLabel(l4);
jump1.setLabel(l4);
var code = c.generate();
// const 値はコード生成後にセットする。
const0.setLabel(l0);
const1.setLabel(l1);
const2.setLabel(l2);
const3.setLabel(l3);
for (var i = -1; i < 5; ++i) {
var r = code.run(i);
System.println(r);
}
結果。
-1
102
103
104
105
-1
コード出力はこんな感じ。こちらは試しに Windows 版で出してみた。
0: 53 push rbx
1: 56 push rsi
2: 57 push rdi
3: 48 8b d9 mov rbx, rcx
6: 48 8b f2 mov rsi, rdx
9: 49 8b f8 mov rdi, r8
c: 4c 8b 4c 24 b0 mov r9, [rsp-0x50]
11: 48 83 ec 50 sub rsp, 0x50
15: 48 c7 c2 ff ff ff ff mov rdx, 0xffffffffffffffff
1c: 48 83 fb 00 cmp rbx, 0x0
20: 0f 8c 94 00 00 00 jl 0xba
26: 48 83 fb 03 cmp rbx, 0x3
2a: 0f 8f 8a 00 00 00 jg 0xba
30: 49 b9 95 ff 57 61 89 01 00 00 mov r9, 0x1896157ff95
3a: 4c 89 4c 24 20 mov [rsp+0x20], r9
3f: 49 b9 a7 ff 57 61 89 01 00 00 mov r9, 0x1896157ffa7
49: 4c 89 4c 24 28 mov [rsp+0x28], r9
4e: 49 b9 b9 ff 57 61 89 01 00 00 mov r9, 0x1896157ffb9
58: 4c 89 4c 24 30 mov [rsp+0x30], r9
5d: 49 b9 cb ff 57 61 89 01 00 00 mov r9, 0x1896157ffcb
67: 4c 89 4c 24 38 mov [rsp+0x38], r9
6c: 48 8d 44 24 20 lea rax, [rsp+0x20]
71: 48 6b db 08 imul rbx, rbx, 0x8
75: 48 03 c3 add rax, rbx
78: 48 8b 00 mov rax, [rax]
7b: ff e0 jmp rax
7d: 48 c7 c2 66 00 00 00 mov rdx, 0x66
84: 48 89 d0 mov rax, rdx
87: 48 83 c4 50 add rsp, 0x50
8b: 5f pop rdi
8c: 5e pop rsi
8d: 5b pop rbx
8e: c3 ret
8f: 48 c7 c2 67 00 00 00 mov rdx, 0x67
96: 48 89 d0 mov rax, rdx
99: 48 83 c4 50 add rsp, 0x50
9d: 5f pop rdi
9e: 5e pop rsi
9f: 5b pop rbx
a0: c3 ret
a1: 48 c7 c2 68 00 00 00 mov rdx, 0x68
a8: 48 89 d0 mov rax, rdx
ab: 48 83 c4 50 add rsp, 0x50
af: 5f pop rdi
b0: 5e pop rsi
b1: 5b pop rbx
b2: c3 ret
b3: 48 c7 c2 69 00 00 00 mov rdx, 0x69
ba: 48 89 d0 mov rax, rdx
bd: 48 83 c4 50 add rsp, 0x50
c1: 5f pop rdi
c2: 5e pop rsi
c3: 5b pop rbx
c4: c3 ret
7b 行目の jmp rax
がポイント。テーブルを静的に定義できるようになればジャンプテーブルとして機能するようになるかと(今は簡単にできる方法が無い...)。
4 つ以上の引数の例
ちょっと面倒くさいが、4 つ以上引数を渡したい場合は、ローカル変数領域に値を格納し、そのアドレス(ポインタ)を引数として渡す。以下の例では、最初に引数をローカル変数領域にセットするためのフック関数を経由させている。ちなみに、ローカル変数は全て 8 バイトで確保されるため、直接 Jit.MEM1()
などでアクセスする場合のオフセットは 8 の倍数でないと合わないので注意。
var c = new Jit.Compiler();
var entry1 = c.enter();
c.mov(Jit.VAR(0), Jit.S0);
c.mov(Jit.VAR(1), Jit.IMM(3));
c.mov(Jit.VAR(2), Jit.IMM(2));
c.mov(Jit.VAR(3), Jit.IMM(1));
c.localp(Jit.R0);
var call1 = c.call();
c.ret(Jit.R0);
var entry2 = c.enter();
c.mov(Jit.R1, Jit.S0);
c.mov(Jit.S0, Jit.MEM1(Jit.R1, 0));
var jump0 = c.ge(Jit.S0, Jit.MEM1(Jit.R1, 8));
c.ret(Jit.S0);
var l1 = c.label();
c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 16));
c.mov(Jit.VAR(0), Jit.R3);
c.mov(Jit.VAR(1), Jit.IMM(3));
c.mov(Jit.VAR(2), Jit.IMM(2));
c.mov(Jit.VAR(3), Jit.IMM(1));
c.localp(Jit.R0);
c.call(entry2);
c.mov(Jit.S1, Jit.R0);
c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 24));
c.mov(Jit.VAR(0), Jit.R3);
c.mov(Jit.VAR(1), Jit.IMM(3));
c.mov(Jit.VAR(2), Jit.IMM(2));
c.mov(Jit.VAR(3), Jit.IMM(1));
c.localp(Jit.R0);
c.call(entry2);
c.add(Jit.R0, Jit.R0, Jit.S1);
c.ret(Jit.R0);
jump0.setLabel(l1);
call1.setLabel(entry2);
var code = c.generate();
for (var i = 1; i <= 42; ++i) {
var tmr = new SystemTimer();
var r = code.run(i);
System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}
出力はさっきと同じ。
Double の引数と復帰値
Double 紹介していないのでそれも。こちらもフィボナッチでいきましょう。しかし、俺はフィボナッチ大好きだな。気づかなかったけど。0.1 刻みバージョンです。
var c = new Jit.Compiler();
var entry1 = c.enter(Jit.ArgType.FP);
c.mov(Jit.FR0, Jit.IMM(0.3));
var jump0 = c.ge(Jit.FS0, Jit.FR0);
c.ret(Jit.FS0);
var l1 = c.label();
c.mov(Jit.FR0, Jit.IMM(0.2));
c.sub(Jit.FR0, Jit.FS0, Jit.FR0);
c.call(entry1);
c.mov(Jit.FS1, Jit.FR0);
c.mov(Jit.FR0, Jit.IMM(0.1));
c.sub(Jit.FR0, Jit.FS0, Jit.FR0);
c.call(entry1);
c.add(Jit.FR0, Jit.FR0, Jit.FS1);
c.ret(Jit.FR0);
jump0.setLabel(l1);
var code = c.generate();
for (var i = 0.1; i < 3.5; i += 0.1) {
var tmr = new SystemTimer();
var r = code.frun(i);
System.println("[%8.3f] fib(%3.1f) = %.1f" % tmr.elapsed() % i % r);
}
浮動小数点数の即値は直接比較メソッドで使えるようにしていないので(すればいいんだけど)一旦レジスタに格納して使う必要がある。
frun()
することで Double 値を受け取れる。結果は以下の通り。
[ 0.000] fib(0.1) = 0.1
[ 0.000] fib(0.2) = 0.2
[ 0.000] fib(0.3) = 0.3
[ 0.000] fib(0.4) = 0.5
[ 0.000] fib(0.5) = 0.8
[ 0.000] fib(0.6) = 1.3
[ 0.000] fib(0.7) = 2.1
[ 0.000] fib(0.8) = 3.4
[ 0.000] fib(0.9) = 5.5
[ 0.000] fib(1.0) = 8.9
[ 0.000] fib(1.1) = 14.4
[ 0.000] fib(1.2) = 23.3
[ 0.000] fib(1.3) = 37.7
[ 0.000] fib(1.4) = 61.0
[ 0.000] fib(1.5) = 98.7
[ 0.000] fib(1.6) = 159.7
[ 0.000] fib(1.7) = 258.4
[ 0.000] fib(1.8) = 418.1
[ 0.000] fib(1.9) = 676.5
[ 0.000] fib(2.0) = 1094.6
[ 0.000] fib(2.1) = 1771.1
[ 0.000] fib(2.2) = 2865.7
[ 0.000] fib(2.3) = 4636.8
[ 0.000] fib(2.4) = 7502.5
[ 0.000] fib(2.5) = 12139.3
[ 0.001] fib(2.6) = 19641.8
[ 0.001] fib(2.7) = 31781.1
[ 0.002] fib(2.8) = 51422.9
[ 0.003] fib(2.9) = 83204.0
[ 0.004] fib(3.0) = 134626.9
[ 0.006] fib(3.1) = 217830.9
[ 0.015] fib(3.2) = 352457.8
[ 0.020] fib(3.3) = 570288.7
[ 0.027] fib(3.4) = 922746.5
出力コードは以下の通り。これも Windows 版。浮動小数点数を引き渡すために、最初に簡単なフック関数がある。SLJIT は関数のエントリポイントでの引数で浮動小数点数を指定できないので、こういう形で回避している。
そういう意味でも、SLJIT を直接使うよりこっちを使うほうが色々面倒見てくれて良い。ローカル変数領域で必要なサイズを自動的に計算したり、非破壊レジスタのための一時保存コードなども必要な数行うように自動で計算もさせているので。
0: 53 push rbx
1: 56 push rsi
2: 57 push rdi
3: 48 8b d9 mov rbx, rcx
6: 48 8b f2 mov rsi, rdx
9: 49 8b f8 mov rdi, r8
c: 4c 8b 4c 24 d0 mov r9, [rsp-0x30]
11: 48 83 ec 30 sub rsp, 0x30
15: 0f 29 74 24 20 movaps [rsp+0x20], xmm6
1a: f2 0f 10 03 movsd xmm0, qword [rbx]
1e: 48 89 f2 mov rdx, rsi
21: 49 89 f8 mov r8, rdi
24: 48 89 c1 mov rcx, rax
27: e8 0d 00 00 00 call 0x39
2c: 0f 28 74 24 20 movaps xmm6, [rsp+0x20]
31: 48 83 c4 30 add rsp, 0x30
35: 5f pop rdi
36: 5e pop rsi
37: 5b pop rbx
38: c3 ret
39: 53 push rbx
3a: 56 push rsi
3b: 57 push rdi
3c: 48 8b d9 mov rbx, rcx
3f: 48 8b f2 mov rsi, rdx
42: 49 8b f8 mov rdi, r8
45: 4c 8b 4c 24 b0 mov r9, [rsp-0x50]
4a: 48 83 ec 50 sub rsp, 0x50
4e: 0f 29 74 24 20 movaps [rsp+0x20], xmm6
53: f2 0f 11 6c 24 38 movsd [rsp+0x38], xmm5
59: f2 0f 10 f0 movsd xmm6, xmm0
5d: 49 b9 33 33 33 33 33 33 d3 3f mov r9, 0x3fd3333333333333
67: 4c 89 4c 24 40 mov [rsp+0x40], r9
6c: f2 0f 10 44 24 40 movsd xmm0, qword [rsp+0x40]
72: 66 0f 2e f0 ucomisd xmm6, xmm0
76: 73 17 jae 0x8f
78: f2 0f 10 c6 movsd xmm0, xmm6
7c: f2 0f 10 6c 24 38 movsd xmm5, qword [rsp+0x38]
82: 0f 28 74 24 20 movaps xmm6, [rsp+0x20]
87: 48 83 c4 50 add rsp, 0x50
8b: 5f pop rdi
8c: 5e pop rsi
8d: 5b pop rbx
8e: c3 ret
8f: 49 b9 9a 99 99 99 99 99 c9 3f mov r9, 0x3fc999999999999a
99: 4c 89 4c 24 40 mov [rsp+0x40], r9
9e: f2 0f 10 44 24 40 movsd xmm0, qword [rsp+0x40]
a4: f2 0f 10 e6 movsd xmm4, xmm6
a8: f2 0f 5c e0 subsd xmm4, xmm0
ac: f2 0f 11 e0 movsd xmm0, xmm4
b0: 48 89 c1 mov rcx, rax
b3: e8 81 ff ff ff call 0x39
b8: f2 0f 10 e8 movsd xmm5, xmm0
bc: 49 b9 9a 99 99 99 99 99 b9 3f mov r9, 0x3fb999999999999a
c6: 4c 89 4c 24 40 mov [rsp+0x40], r9
cb: f2 0f 10 44 24 40 movsd xmm0, qword [rsp+0x40]
d1: f2 0f 10 e6 movsd xmm4, xmm6
d5: f2 0f 5c e0 subsd xmm4, xmm0
d9: f2 0f 11 e0 movsd xmm0, xmm4
dd: 48 89 c1 mov rcx, rax
e0: e8 54 ff ff ff call 0x39
e5: f2 0f 58 c5 addsd xmm0, xmm5
e9: f2 0f 10 6c 24 38 movsd xmm5, qword [rsp+0x38]
ef: 0f 28 74 24 20 movaps xmm6, [rsp+0x20]
f4: 48 83 c4 50 add rsp, 0x50
f8: 5f pop rdi
f9: 5e pop rsi
fa: 5b pop rbx
fb: c3 ret
おわりに
JIT 面白いですね。これでパーサ・コンビネータとか実装して組み合わせたらたら、ちょっとした JIT 付き言語処理系が作れますね。そういう道を目指してもいいかも。
おそらく想定する使い方としては次の 2 通りでしょうか。
- Kinx のライブラリ作成の際に数値計算等の範囲で JIT 化し、高速化する。
- DSL(ドメイン固有言語)やオレオレ言語のホストとなり、バックエンドの出力に使う。
ではまた。