「60分でできる!JITコンパイラ自作入門」

(0)

(1) 64bit-インタプリタ編 [61行]

 Sub_AI(SP,16); // SP = SP - 16;
 Mov_RI(R0,0); // R0 = 0;
 Sto_RMD(R0,SP,0); // [SP+0] = R0;
 Sto_RMD(R0,SP,8); // [SP+8] = R0;
Lb_I(1); // L_1:
 Lod_RMD(R0,SP,8); // R0 = [SP+8];
 Lod_RMD(R1,SP,0); // R1 = [SP+0];
 Add_RR(R0,R1); // R0 += R1;
 Sto_RMD(R0,SP,0); // [SP+0] = R0;
 Lod_RMD(R0,SP,8); // R0 = [SP+8];
 Add_RI(R0,1); // R0 += 1;
 Sto_RMD(R0,SP,8); // [SP+8] = R0;
 CmpJlt_RII(R0,100000000,1); // if (R0 < 100000000) goto L_1;
 Lod_RMD(R0,SP,0); // R0 = [SP+0];
 Add_AI(SP,16); // SP = SP + 16;
 End(); // R0に計算結果が入っているので、それを表示して終了;

// ちなみに上記のコードは下記のソースコードをkccにコンパイルさせて生成したものです. 
// int s, i; s = 0;
// for (i = 0; i < 100000000; i++) { s = s + i; }
// つまり、0から99999999までの和を計算しています.
Mov_RI(Rx,const);整数レジスタ(Rx)に定数を代入Rx = const;
Lod_RMD(Rx,Ax,disp);メモリから値を読んで整数レジスタに入れるRx = [Ax + disp];
Sto_RMD(Rx,Ax,disp);整数レジスタの値をメモリに書き込む[Ax + disp] = Rx;
Add_RR(Rx,Ry);整数レジスタ同士の加算Rx += Ry;
Add_RI(Rx,const);整数レジスタに定数を加算Rx += const;
Add_AI(Ax,const);アドレスレジスタ(Ax)に定数を加算Ax += const;
Sub_AI(Ax,const);アドレスレジスタに定数を減算Ax -= const;
Lb_I(x);分岐先指定用のラベル定義命令(ラベルには名前は付けられず番号で区別する)L_x:
CmpJlt_RII(Rx,const,y);条件分岐命令if (Rx < const) goto L_y;
End();R0の値を表示して終了

#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <inttypes.h>

struct Code { int64_t op, a, b, c; };
enum { Mov_RI=1, Lod_RMD, Sto_RMD, Add_RR, Add_RI, Add_AI, Sub_AI, Lb_I, CmpJlt_RII, End };
enum { R0=0, R1=1, SP=6 };

int main()
{
  static struct Code t[17] = {
    { Sub_AI,  SP, 16, -0 }, // -0は命令としては意味を持ってないパラメータ.
    { Mov_RI,  R0, 0,  -0 },
    { Sto_RMD, R0, SP, 0  },
    { Sto_RMD, R0, SP, 8  },
    { Lb_I,    1,  -0, -0 },
    { Lod_RMD, R0, SP, 8  },
    { Lod_RMD, R1, SP, 0  },
    { Add_RR,  R0, R1, -0 },
    { Sto_RMD, R0, SP, 0  },
    { Lod_RMD, R0, SP, 8  },
    { Add_RI,  R0, 1,  -0 },
    { Sto_RMD, R0, SP, 8  },
    { CmpJlt_RII, R0, 100000000, 1 },
    { Lod_RMD, R0, SP, 0  },
    { Add_AI,  SP, 16, -0 },
    { End,     -0, -0, -0 },
    { 0,       -0, -0, -0 }
  };
  int64_t lb[256], r[8], a[8], pc; 
  char m[256]; // メモリ.

  // Lb_I(x)命令を探して位置をlb[]に格納する.
  for (pc = 0; t[pc].op > 0; pc++) {
    if (t[pc].op == Lb_I) { lb[t[pc].a] = pc + 1; }
    // 高速化のために、ラベル命令の次の命令の位置を登録している.
  }

  // 実行開始.
  pc = 0; a[6] = 256;
  clock_t tm0 = clock();
  for (;;) {
    struct Code *tp = &t[pc]; int64_t *mp;
    switch (tp->op) {
    case Mov_RI:  r[tp->a] = tp->b; pc++; continue;
    case Lod_RMD: mp = (int64_t *) (m + a[tp->b] + tp->c); r[tp->a] = *mp; pc++; continue;
    case Sto_RMD: mp = (int64_t *) (m + a[tp->b] + tp->c); *mp = r[tp->a]; pc++; continue;
    case Add_RR:  r[tp->a] += r[tp->b]; pc++; continue;
    case Add_RI:  r[tp->a] += tp->b; pc++; continue;
    case Add_AI:  a[tp->a] += tp->b; pc++; continue;
    case Sub_AI:  a[tp->a] -= tp->b; pc++; continue;
    case Lb_I:    pc++; continue;
    case CmpJlt_RII: if (r[tp->a] < tp->b) { pc = lb[tp->c]; } else { pc++; } continue;
    case End:     goto fin;
    }
  }
fin:
  printf("R0=%" PRId64 " (%.3f[sec])\n", r[0], (clock() - tm0) / (double) CLOCKS_PER_SEC);
  return 0;
}

(2) 64bit-JITコンパイラ編(x64用のコード生成) [69行]

#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <inttypes.h>

struct Code { int64_t op, a, b, c; };
enum { Mov_RI=1, Lod_RMD, Sto_RMD, Add_RR, Add_RI, Add_AI, Sub_AI, Lb_I, CmpJlt_RII, End };
enum { R0=0, R1=1, SP=6 };

#include <windows.h>
void *mallocRWX(int siz) { return VirtualAlloc(0, siz, MEM_COMMIT, PAGE_EXECUTE_READWRITE); } // [参考] https://essen.osask.jp/?a21_txt02 の(4).

void set32(char *p, int64_t i) { p[0] = i & 0xff; p[1] = (i >> 8) & 0xff; p[2] = (i >> 16) & 0xff; p[3] = (i >> 24) & 0xff; }
void set64(char *p, int64_t i) { set32(p, i); set32(p + 4, i >> 32); }

int main()
{
  static struct Code t[17] = {
    { Sub_AI,  SP, 16, -0 }, // -0は命令としては意味を持ってないパラメータ.
    { Mov_RI,  R0, 0,  -0 },
    { Sto_RMD, R0, SP, 0  },
    { Sto_RMD, R0, SP, 8  },
    { Lb_I,    1,  -0, -0 },
    { Lod_RMD, R0, SP, 8  },
    { Lod_RMD, R1, SP, 0  },
    { Add_RR,  R0, R1, -0 },
    { Sto_RMD, R0, SP, 0  },
    { Lod_RMD, R0, SP, 8  },
    { Add_RI,  R0, 1,  -0 },
    { Sto_RMD, R0, SP, 8  },
    { CmpJlt_RII, R0, 100000000, 1 },
    { Lod_RMD, R0, SP, 0  },
    { Add_AI,  SP, 16, -0 },
    { End,     -0, -0, -0 },
    { 0,       -0, -0, -0 }
  };
  int64_t lb[256], r[8], pc, pass, rip; 
  char *tx = mallocRWX(65536); // とりあえず64KB.

  // 方針: R0はRAX, R1はRCXに割り当てる. SPはRSPに割り当てる. それ以外のレジスタ指定はないと仮定(簡単のために).
  // JITコンパイル開始.
  for (pass = 0; pass < 2; pass++) { // あえて2回まわす.
    rip = 0;
    for (pc = 0; t[pc].op > 0; pc++) {
      struct Code *tp = &t[pc];
      switch (tp->op) {
      case Mov_RI:  tx[rip + 0] = 0x48; tx[rip + 1] = 0xb8 + tp->a; set64(&tx[rip + 2], tp->b); rip += 10; continue;
      case Lod_RMD: tx[rip + 0] = 0x48; tx[rip + 1] = 0x8b; tx[rip + 2] = 0x84 + tp->a * 8; tx[rip + 3] = 0x24; set32(&tx[rip + 4], tp->c); rip += 8; continue; // tp->b == SP のみ想定.
      case Sto_RMD: tx[rip + 0] = 0x48; tx[rip + 1] = 0x89; tx[rip + 2] = 0x84 + tp->a * 8; tx[rip + 3] = 0x24; set32(&tx[rip + 4], tp->c); rip += 8; continue; // tp->b == SP のみ想定.
      case Add_RR:  tx[rip + 0] = 0x48; tx[rip + 1] = 0x03; tx[rip + 2] = 0xc0 + tp->a * 8 + tp->b; rip += 3; continue;
      case Add_RI:  tx[rip + 0] = 0x48; tx[rip + 1] = 0xba; set64(&tx[rip + 2], tp->b); tx[rip + 10] = 0x48; tx[rip + 11] = 0x03; tx[rip + 12] = 0xc2 + tp->a * 8; rip += 13; continue;
      case Add_AI:  tx[rip + 0] = 0x48; tx[rip + 1] = 0x81; tx[rip + 2] = 0xc4 + 0 * 8; set32(&tx[rip + 3], tp->b); rip += 7; continue; // tp->a == SP のみ想定.
      case Sub_AI:  tx[rip + 0] = 0x48; tx[rip + 1] = 0x81; tx[rip + 2] = 0xc4 + 5 * 8; set32(&tx[rip + 3], tp->b); rip += 7; continue; // tp->a == SP のみ想定.
      case Lb_I:    lb[tp->a] = rip; continue;
      case CmpJlt_RII: tx[rip + 0] = 0x48; tx[rip + 1] = 0xba; set64(&tx[rip + 2], tp->b); tx[rip + 10] = 0x48; tx[rip + 11] = 0x3b; tx[rip + 12] = 0xc2 + tp->a * 8;
        tx[rip + 13] = 0x0f; tx[rip + 14] = 0x8c; set32(&tx[rip + 15], lb[tp->c] - (rip + 19)); rip += 19; continue;
      case End:     tx[rip + 0] = 0xc3; rip++; continue;
      }
    }
  }
  // int i; for (i = 0; i < rip; i++) printf("%02x ", ((unsigned char *) tx)[i]); // デバッグ用.

  // 実行開始.
  int64_t (*f)() = (void *) tx;
  clock_t tm0 = clock();
  r[0] = f();
  printf("R0=%" PRId64 " (%.3f[sec])\n", r[0], (clock() - tm0) / (double) CLOCKS_PER_SEC);
  return 0;
}

(3) 今回使った機械語

48 bx [8バイト] : RAX/RCX/RDXに定数を代入. x=8:RAX, x=9:RCX, x=a:RDX.
48 03 xx : レジスタをレジスタに加える演算( a += b; ). xx=c0+b+a*8.
48 81 xx [4バイト] : レジスタと32bit整数定数の演算. 定数が32bitで収まると分かっているのならこれを使った方が速い. xxの部分で対象レジスタや演算タイプを指定する.
48 3b xx : レジスタ同士の比較.
0f 8c [4バイト] : 条件分岐(8cならJL)
c3 : return;
48 8b xx 24 [disp] : レジスタ=[RSP+disp]; xx=84+a*8.
48 89 xx 24 [disp] : [RSP+disp]=レジスタ; xx=84+a*8.

(4) レジスタだけで計算するようにすれば、インタプリタでもJITコンパイラでも速くなります(2倍速くらい)。

 Mov_RI(R0,0);
 Mov_RI(R1,0);
Lb_I(1); // L_1:
 Add_RR(R0,R1); // R0 += R1;
 Add_RI(R1,1); // R1 += 1;
 CmpJlt_RII(R1,100000000,1); // if (R1 < 100000000) goto L_1;
 End(); // R0に計算結果が入っているので、それを表示して終了;

(5) 理解を深めるためのQ&A


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS