* Essen Rev2 JIT00 -(by [[K]], 2017.07.31) ** 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レイヤ次第(使えばきっと高速化には寄与する)。 -基本構文 命令 パラメータ1 パラメータ2 ... -人間にとって書きやすい構文にしようとしていない。どうせこれはJIT01から渡されてくる中間言語でしかない。 --だからコメントやマクロなどはない。 -パラメータはスペース区切り(コンマはいらない) -命令の命名規則 --最初の一文字はnかs。nはノーマル。sはスピード優先。 --二文字目はパラメータの数。 --末尾には型を表す接尾子が入る場合がある。 -例1: n0sysEnt // JITコードの先頭に入れるべきコード、レジスタを初期化する s2cp_s32 g0 c0 // g0に定数0を代入 n1label L0 // ここをL0とする s2add_s32 g0 g0 c1 // g0 += 1 s4cjmp_s32 g0 c1000000000 1 L0 // 条件ジャンプ命令 if (g0 != 1000000000) goto L0 n0sysRet // JITコードから帰る場合に入れるべきコード -[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 --s4cjmp_f64 (ポインタを使ったjmpは想定しない) --s4cset_s32_f64 -ポインタ演算 --未設計 ** x86(32bit)版の仕様 -こうでなければいけないということではないが、とりあえず最初のバージョンではこうやって構成した、という例として: -変数は16バイト --型(32bit)、補助属性(32bit)、値(最大64bit) --値が64bitに収まらない場合や可変長の場合は、ポインタを入れておく。 -EAX: テンポラリデータレジスタ -EBP: ローカル変数のベース -EBX, ESI, EDI, ECX, EDX: pかiを割り当てる //-EAX: テンポラリデータレジスタ //-EDI: グローバル変数のベース //-EBP: ローカル変数のベース //-ESI: ワークエリアのベース //-EBX: p0 //-ECX: テンポラリポインタ(p1) //-EDX: テンポラリ //--p2~p31はワークエリア内にある //--i0~i31はワークエリア内にある //--f0~f5はFPUの中にあり、f6~f31はワークエリア内にある -EAX: テンポラリデータレジスタ -EDI: グローバル変数のベース -EBP: ローカル変数のベース -ESI: ワークエリアのベース -EBX: p0 -ECX: テンポラリポインタ(p1) -EDX: テンポラリ --p2~p31はワークエリア内にある --i0~i31はワークエリア内にある --f0~f5はFPUの中にあり、f6~f31はワークエリア内にある -例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は依存関係に注意しつつ並列実行してくれるようだ。