Essen Rev2 JIT00
JIT00で提供する機能
- 「即値が使えない命令」のような例外をなくす
- ラベルを使えるようにする
- CPUに依存するレイヤをここに集約する(JIT01以降はCPUに依存しない)
- 実際のCPUのレジスタ数がいくつであるかなどの制約を受けずに済むようにする
基本仕様
- c# : 即値
- l# : ローカル変数(#=0~)
- g# : グローバル変数
- L# : 分岐先指定などに使うラベル
- p# : ポインタレジスタ(p0~p63)
- i# : s32以上レジスタ(i0~i63)
- f# : f64以上レジスタ(f0~f63)
- ローカル変数、グローバル変数には、どんな型の値でも代入できる。なぜならEssenは値が型を持つ言語だから。
- ポインタレジスタはポインタしか代入できない。
- s32以上レジスタは整数しか代入できない。
- f64以上レジスタは浮動小数点しか代入できない。
- p#, i#, f#はグローバル変数で、これをうまく使いこなせるかどうかはJIT01レイヤ次第(使えばきっと高速化には寄与する)。
- [Q]人間が書くわけじゃない中間言語なのに、どうしてバイナリにしなかったの?テキストなんて生成するのも解釈するのも手間じゃないか。
- [A]その意見には全面的に賛成します。まあひとまずはデバッグを楽にするためにテキストでやっています、くらいの感じで。・・・将来的にはバイナリ化するかもしれません。
命令一覧
- システム命令
- n0sysEnt
- n0sysRet
- n0reg
- n0reg "ppif" みたいに使う。「可能であればp00,p01,i00,f00の順に実レジスタに割り当ててほしい」という意思表示。
- 2項演算命令
- s2cp_s32
- s2cp_f64
- s2cnv_f64_s32
- s2cnv_s32_f64
- s2not_s32 // xor -1 で代用できるので、消すかもしれない.
- s2neg_s32 // mul -1 で代用できるので、消すかもしれない.
- 3項演算命令
- s3or_s32
- s3xor_s32
- s3and_s32
- s3add_s32
- s3sub_s32
- s3mul_s32
- s3shl_s32
- s3shr_s32 (符号付き右シフト)
- s3div_s32
- s3mod_s32
- s3add_f64
- s3sub_f64
- s3mul_f64
- s3div_f64
- 条件分岐命令など
- n1align
- n1label
- n1jmp
- n1call_ext
- s4cjmp_s32 (ポインタを使ったjmpは想定しない)
- s4cset_s32_s32
- s4cjmp_f64 (ポインタを使ったjmpは想定しない)
- s4cset_s32_f64
x86(32bit)版の仕様
- こうでなければいけないということではないが、とりあえず最初のバージョンではこうやって構成した、という例として:
- 変数は16バイト
- 型(32bit)、補助属性(32bit)、値(最大64bit)
- 値が64bitに収まらない場合や可変長の場合は、ポインタを入れておく。
- EAX: テンポラリデータレジスタ
- EBP: ローカル変数のベース
- EBX, ESI, EDI, ECX, EDX: pかiを割り当てる
- 例1は以下のような機械語になる(2017.07.28時点)
60 PUSHAD n0sysEnt
BF E8 24 48 00 EDI=....
B8 00 00 00 00 EAX=0 s2cp_s32 g0 i0
89 87 08 00 00 00 [EDI+8]=EAX
n1label L0
8B 87 08 00 00 00 EAX=[EDI+8] s2add_s32 g0 g0 i1
40 EAX++
89 87 08 00 00 00 [EDI+8]=EAX
3D 00 CA 9A 3B CMP(EAX,0x3b9aca00) s4cjmp_s32 g0 i1000000000 1 L0 // EAX=[EDI+8]を自動で省略している.
0F 85 E8 FF FF FF JNE L0
61 POPAD n0sysRet
C3 RET
- [Q]もっと短い機械語を生成しなくていいのか?
- [A]もちろんやりたいが、今は他を先に作るべきだと思っているので、今はまだそこには着手しない。
- [Q]・・・で、肝心の性能は?
- [A]Core-i7で実験した範囲では、gccで-O3したのと同じくらいの性能が出る。これは使える!!
もうちょっと細かい解説(1)
- 基本的には以下のルールでコンパイルされている。
s2cp_s32 a b → MOV(EAX, b); MOV(a, EAX);
s2add_s32 a b c → MOV(EAX, b); ADD(EAX, c); MOV(a, EAX); // subやandなどもほぼ同様.
s4cjmp_s32 a b c d → MOV(EAX, a); CMP(EAX, b); jcc(d);
- 見ての通りEAXしか使ってない。きわめてシンプルだ。
- ただしこれだけだと速度が出ない、なぜなら、
1: MOV(EAX, [EDI+8]);
2: INC(EAX);
3: MOV([EDI+8], EAX);
4: MOV(EAX, [EDI+8]);
5: CMP(EAX, 0x3b9aca00);
6: JNE(L0);
- のときに、3行目の書き込みが終わらないと4行目の読み込みが実行できず、そこでCPUが待たされてしまうから。
- ということで、JITコンパイラは不要な4行目を削除している(ストアした直後に同じレジスタに同じアドレスでロード命令を生成しそうになったら生成を中止しているだけ)。この程度の最適化だけで、多くのケースでgcc -O3並みの速度を実現できる。
- EAXばかり使っていてもCPUはおそらくちゃんと依存関係を理解してレジスタリネーミングをして並列実行していると思われる。
もうちょっと細かい解説(2)
- レジスタ変数: zx:f0, zy:f1, xx:f2, yy:f3, tmp:f4, tx:f4, ty:f5
1: s3mul_f64 f2 f0 f0 xx = zx * zx
2: s3mul_f64 f3 f1 f1 yy = zy * zy
3: s3add_f64 f4 f3 f2 tmp = yy + xx
4: s4cjmp_f64 f4 c4.0 5 L5 if (tmp > 4) goto L5
5: s3sub_f64 f4 f2 f3 tx = xx - yy
6: s3mul_f64 f5 f0 f1 ty = zx * zy * 2.0
7: s3add_f64 f5 f5 f5
8: s3add_f64 f0 f4 g8 zx = tx + cx
9: s3add_f64 f1 f5 g9 zy = ty + cy
- このコードから以下のバイナリを生成する(でもここでは機械語部分は省略してアセンブラ表記のみで)。
01: FLD ST(0) // f0
02: FMUL ST(1) // f0
03: FSTP ST(3) // f2
04: FLD ST(1) // f1
05: FMUL ST(2) // f1
06: FSTP ST(4) // f3
07: FLD ST(3) // f3
08: FADD ST(3) // f2
09: FSTP ST(5) // f4
10: FLD ST(4) // f4
11: FCOMP [...] // 4.0
12: FNSTSW AX
13: SAHF
14: JA L5
(以下略)
- ここでEssenRev2のJIT00程度でも、以下の最適化が可能だと思われる(まだやってないけどいつかやる予定)。
- 06行目をFSTに変更し、07行目を省略する
- 09行目をFSTに変更し、10行目を省略する
- このようにレジスタ内で演算するようにすると、CPUは依存関係に注意しつつ並列実行してくれるようだ。
|