0
0

More than 1 year has passed since last update.

Kinx ライブラリ - JIT コンパイラ・ライブラリ

Last updated at Posted at 2020-06-29

Kinx ライブラリ - JIT コンパイラ・ライブラリ

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。JIT コンパイルのためのライブラリを作ってみました。

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 しか確認して(できて)いませんので、そこは悪しからずご了承ください。

公式? 説明文書

私が知る限り、参考になる文書は以下程度しか見つかりませんでした。

参考にはなります。

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.R0Jit.R5 汎用レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。
Jit.S0Jit.S5 汎用レジスタ。別関数呼び出し後に破棄されない保証。
Jit.FR0Jit.FR5 浮動小数点レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。
Jit.FS0Jit.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) dstop1 を代入するコードを出力する。浮動小数点とそれ以外の型は自動的に認識する。
Jit.Compiler#neg(dst, op1) op1 の符号反転した結果を dst に格納するコードを出力する。
Jit.Compiler#clz(dst, op1) op1 の先頭から 0 であるビットの数を数え、dst に格納するコードを出力する。
Jit.Compiler#add(dst, op1, op2) op1op2 を加算した結果を dst に格納するコードを出力する。
Jit.Compiler#sub(dst, op1, op2) op1op2 を減算した結果を dst に格納するコードを出力する。
Jit.Compiler#mul(dst, op1, op2) op1op2 を乗算した結果を dst に格納するコードを出力する。
Jit.Compiler#div(dst, op1, op2) 浮動小数点数のみ、op1op2 を除算した結果を 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) op1op2 でビット AND した値を dst に格納するコードを出力する。
Jit.Compiler#or(dst, op1, op2) op1op2 でビット OR した値を dst に格納するコードを出力する。
Jit.Compiler#xor(dst, op1, op2) op1op2 でビット XOR した値を dst に格納するコードを出力する。
Jit.Compiler#shl(dst, op1, op2) op1op2 ビット分、左シフトした値を dst に格納するコードを出力する。
Jit.Compiler#lshr(dst, op1, op2) op1op2 ビット分、論理右シフトした値を dst に格納するコードを出力する。
Jit.Compiler#ashr(dst, op1, op2) op1op2 ビット分、算術右シフトした値を 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)

実際問題としては受け取るレジスタのビット列が同じになるため SWUW は変わらないが、もしかしたら将来何か違いを出すかもしれない。尚、最後の引数から 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 通りでしょうか。

  1. Kinx のライブラリ作成の際に数値計算等の範囲で JIT 化し、高速化する。
  2. DSL(ドメイン固有言語)やオレオレ言語のホストとなり、バックエンドの出力に使う。

ではまた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0