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 (ポインタを使った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は依存関係に注意しつつ並列実行してくれるようだ。

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-08-25 (金) 11:25:21 (2429d)