JIT 予備実験1

CONSTANT 命令

CONSTANT 命令 を -S で見てみる。
ac_ レジスタオペランドを放り込む。

        CASE(CONSTANT)
        {
            asm volatile(" \t # -- CONSTANT start");
            ac_ = fetchOperand();
            asm volatile(" \t # -- CONSTANT end");
            NEXT1;
        }
.loc 9 50 0
movq	80(%rsp), %rbx
.LVL809:
movq	48(%rbx), %rax
.LBE14145:
movq	(%rax), %rdx
.LBB14146:
leaq	8(%rax), %rcx
movq	%rcx, 48(%rbx)
.loc 3 312 0
movq	%rdx, 8(%rbx)
.LBE14146:
.LBE14144:
.loc 3 313 0
#APP
# 313 "VM-Run.cpp" 1
 	 # -- CONSTANT end

少し複雑すぎたので

ac_ = Object::makeFixnum(3);

と書き直してみる。

# 311 "VM-Run.cpp" 1
 	 # -- CONSTANT start
# 0 "" 2
.loc 3 312 0
#NO_APP
movq	80(%rsp), %rbx ;; %rbx = this
.LVL809:
movq	$13, 8(%rbx)   ;; this->ac_ = 13
.loc 3 313 0
#APP
# 313 "VM-Run.cpp" 1
 	 # -- CONSTANT end
# 0 "" 2


80(%rsp) は this っぽい。
というわけで、先ほどのコードにコメントをいれると。

.loc 9 50 0
movq	80(%rsp), %rbx ;; %rbx = this
.LVL809:
movq	48(%rbx), %rax ;; %rax = this->pc_
.LBE14145:
movq	(%rax), %rdx   ;; %rdx = *(this->pc_)
.LBB14146:
leaq	8(%rax), %rcx  ;; %rcx = this->pc_ + 8
movq	%rcx, 48(%rbx) ;; this->pc_ = %rcx
.loc 3 312 0
movq	%rdx, 8(%rbx)  ;; this->ac_ = %rdx
.LBE14146:
.LBE14144:
.loc 3 313 0
#APP
# 313 "VM-Run.cpp" 1
 	 # -- CONSTANT end

うむ。理屈は通った。
ところで .loc って何。デバッグ用の情報か。
this が 80(%rsp) に割り当てられている背景が知りたい。

CProcedure

JIT 実験では、コンパイル済みコードを Object hoge(VM* theVM, int argc, const Object* argv) というシグネチャで実行する。
theVM は1つ目の引数なので %rdi だろう。
実験したら

movq	$13, 8(%rdi)

OK。


CONSTANT 3 を発行するだけの CProcedure を返す CProcedure を定義(ややこしい)。

Object scheme::returnJitEx(VM* theVM, int argc, const Object* argv)
{
    ExecutableMemory* mem = new ExecutableMemory(256);
    if (!mem->allocate()) {
        fprintf(stderr, "returnJit Error");
        exit(-1);
    }

    mem->push(0x48); // movq   $0xd,0x8(%rdi)
    mem->push(0xc7);
    mem->push(0x47);
    mem->push(0x08);
    mem->push(0x0d);
    mem->push(0x00);
    mem->push(0x00);
    mem->push(0x00);

    mem->push(0x48);// movq    0x8(%rdi),%rax
    mem->push(0x8b);
    mem->push(0x47);
    mem->push(0x08);

    mem->push(0xc3);// retq
    uint8_t* address = mem->address();
    return Object::makeCProcedure(((Object (*)(VM* vm, int, const Object*))address));
}
(display ((return-jit))) ;; 3

うごいた。

分岐を見てみる

 	 # -- Branch on not null start
# 0 "" 2
#NO_APP
.LBB15377:
.LBB15378:
.loc 10 218 0
movq	64(%rsp), %rsi ;; %rsi = this
.LVL786:
movl	$_ZN6scheme6Object5FalseE, %eax ;; %eax = False
movl	$_ZN6scheme6Object4TrueE, %edx  ;; %edx = True
cmpq	$6, 8(%rsi) ;; ac_.isNil() ?
cmove	%rdx, %rax ;; isNil() に従い %rax = False or True
.LBE15378:
movq	(%rax), %rax
.LBE15377:
.loc 27 1128 0
cmpq	$86, %rax ;; %rax is False ?
.LBB15379:
.loc 27 1127 0
movq	%rax, 8(%rsi) ac = %rax
.LBE15379:
.loc 27 1128 0
je	.L1563 ;; jump 先はあとで見る
movq	64(%rsp), %rcx ;; %rcx = stackEnd_
.LVL787:
addq	$8, 48(%rcx) ;; this->pc_ ++;
.L1023:
.loc 27 1129 0
#APP
# 1129 "VM-Run.cpp" 1
 	 # -- end

......
.L1563:
.LBE17379:
.LBB17380:
.LBB17383:
.LBB17384:
.loc 6 50 0
movq	48(%rsi), %rax ;; %rax = this->pc_
.LBE17384:
movq	(%rax), %rdx ;; %rax = *(this->pc_)
.LBB17385:
addq	$8, %rax  ;; %rax += 8
movq	%rax, 48(%rsi)  this->pc_ = %rax
.loc 27 1128 0
movq	%rdx, 400(%rsp) ;; ここから MOSH_ASSERT による isFixnum
.LBE17385:
.LBE17383:
andl	$3, %edx
subb	$1, %dl
jne	.L1671
leaq	400(%rsp), %rdi ;; 引数に %rdx
.LVL1539:
call	_ZNK6scheme6Object8toFixnumEv ;; 

所感

だいたい分かってきたがいくつか問題点がある。

  • VM の物理レイアウトが変更になると困る(これはコンパイル時にチェックすれば良いかも)
  • 約100命令こうやって手作業で見ていくのはしんどい。ある程度自動化したい。
    • 特にVM命令ないで native に jmp しているとややこしい。どこまでが一命令か分かりづらい。


メンテナンスコストを考えると

  1. gcc -S の結果を各命令毎にぶった切る
    1. gas にコメントを入れられるのでそれを利用する
  2. 各命令をパース
  3. VM レジスタの部分を可変に
  4. コード生成器を作る
  5. 実際に jit コンパイル

って感じかな。あとは全ての命令が jit コンパイル後正しく動くことを確認するユニットテストを書く。


明日は fib でもやってみるかな。