FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

自作Binary Translation型RISC-VエミュレータのTB Lookup and Jumpの実装

QEMUTCG Block Chainingを実装したので、次はもう一つの障害、TB Lookup and Jumpを実装しよう。TB Lookup and Jumpは、TCG Block Chainingではカバーしきれない部分を最適化するものだ。TCG Block Chainingは、あるブロックからあるブロックへ、毎回必ず何度でも同じようにジャンプすることが求められるものだ。つまり、Jump Register命令のようにレジスタの値によってジャンプ先が変わる場合にはこの技術は使えない。ダイレクトジャンプを使用しているからだ。

一方で、TB Lookup and Jumpは、まずはレジスタジャンプを行うレジスタ値をTCGの先頭アドレスをキーにしたLookup Tableで検索し、見つかればその物理アドレスを返すことですぐにジャンプする。これは一度ホストのアセンブリ命令実行状態から制御に戻り、いろいろ処理をしてから再度TCGブロックをロードする場合に比べていくらか処理を短縮化できる。とにかく、テーブルサーチの部分だけはC/C++側の実装に頼るが、一度次のTCGのアドレスを入手できればすぐにジャンプすることで無駄に制御が戻ってしまうことを防ぐ。

f:id:msyksphinz:20201226230033p:plain

これをRustで実装しよう。まず、TCGには以下のようにしてヘルパー関数を呼び出す命令を挿入する。引数はジャンプ対象となるレジスタの値。JR rs1ならばrs1の値を引数として渡す。

        let lookup_fail_label = Rc::new(RefCell::new(TCGLabel::new()));

        tcg_lists.push(TCGOp::new_2op_with_label(TCGOpcode::LOOKUP_PC_AND_JMP, TCGv::new_reg(rs1_addr as u64), imm, Rc::clone(&lookup_fail_label)));
        tcg_lists.push(TCGOp::new_label(Rc::clone(&lookup_fail_label)));
  • TCGOpcode::LOOKPP_PC_AND_JMPは以下のようなx86命令に変換される。
        // Argument 0 : Env
        let self_ptr = emu.head.as_ptr() as *const u8;
        let self_diff = unsafe { self_ptr.offset(0) };
        gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RDI, self_diff as u64, mc);

        // Argument 1 : rd u32
        gen_size += Self::tcg_gen_load_gpr_64bit(emu, X86TargetRM::RSI, source_reg.value, mc);
        if source_offset.value != 0 {
            gen_size += Self::tcg_modrm_64bit_raw_out(X86Opcode::ADD_GV_IMM, X86ModRM::MOD_11_DISP_RSI as u8, 0, mc);
            gen_size += Self::tcg_out(source_offset.value, 4, mc);
        }

        gen_size += Self::tcg_modrm_32bit_out(
            X86Opcode::CALL,
            X86ModRM::MOD_10_DISP_RBP,
            X86TargetRM::RDX,
            mc,
        );
        gen_size += Self::tcg_out(emu.calc_lookup_func_address() as u64, 4, mc);

        // Compare result RAX with zero
        gen_size += Self::tcg_modrm_64bit_out(
            X86Opcode::CMP_GV_EV,
            X86ModRM::MOD_10_DISP_RBP,
            X86TargetRM::RAX,
            mc,
        );
        gen_size += Self::tcg_out(emu.calc_gpr_relat_address(0) as u64, 4, mc);
        gen_size = Self::tcg_gen_jcc(gen_size, X86Opcode::JE_rel16_32, mc, &lookup_fail_label);
        gen_size += Self::tcg_modrm_64bit_raw_out(X86Opcode::JMP_R64_M64, X86ModRM::MOD_11_DISP_RAX as u8, X86TargetRM::RAX as u8, mc);
00007F8F9DF104DC 48BFC8263FDFFF7F0000 movabs    $0x7FFF_DF3F_26C8,%rdi       // 第1引数の設定
00007F8F9DF104E6 488BB510000000       mov       0x10(%rbp),%rsi             // 第2引数の設定
00007F8F9DF104ED FF95F0040000         callq     *0x4F0(%rbp)                // ヘルパー関数へのジャンプ
00007F8F9DF104F3 483B8508000000       cmp       8(%rbp),%rax
00007F8F9DF104FA 0F8403000000         je        0x0000_7F8F_9DF1_0503       // 結果が0であれば普通に制御を戻す
00007F8F9DF10500 48FFE0               jmp       *%rax                       // そうでなければテーブルサーチしたアドレスに直接ジャンプする
00007F8F9DF10503 488B5510             mov       0x10(%rbp),%rdx
00007F8F9DF10507 488BC2               mov       %rdx,%rax
00007F8F9DF1050A 480500000000         add       $0,%rax
00007F8F9DF10510 48898508020000       mov       %rax,0x208(%rbp)
00007F8F9DF10517 48B8F60D008000000000 movabs    $0x8000_0DF6,%rax
00007F8F9DF10521 48898510020000       mov       %rax,0x210(%rbp)
00007F8F9DF10528 48B8CC14000000000000 movabs    $0x14CC,%rax
00007F8F9DF10532 E900000000           jmp       0x0000_7F8F_9DF1_0537
00007F8F9DF10537 48B90F00829E8F7F0000 movabs    $0x7F8F_9E82_000F,%rcx
00007F8F9DF10541 48FFE1               jmp       *%rcx

これにより今回の実装の場合はlookup_guest_pc_to_host()という関数にジャンプするように仕掛けてある。引数はジャンプ先アドレスなので、この引数に基づいてTCGブロックを検索する。ヒットすれば、ジャンプ先の命令はすでにx86命令に変換されているので、その命令が格納されているアドレスを戻り値として返す。そうでなければ0を返す。この場合は0アドレスにはダイレクトジャンプできないという弱点は残るが、これはほとんど発生しないので今回は無視している。

    pub fn lookup_guest_pc_to_host (&self, guest_pc: u64) -> u64 {
        match self.m_tb_text_hashmap.get(&guest_pc) {
            Some((_, mem_map)) => {
                // println!("lookup_guest success! {:016x} => {:016x}", guest_pc, mem_map.borrow().data() as u64);
                mem_map.borrow().data() as u64
            },
            None => {
                // println!("lookup_guest fail! {:016x}", guest_pc);
                0
            }
        }
    }

この方式を採用することで、どれくらい高速化したのかを測定した。

f:id:msyksphinz:20201226225125p:plain

結構速くなったな。最初は4秒近くかかっていたDhrystoneの実行が、2秒程度で終わるまで改善された。なかなか良くなっている。

ただし本家QEMUからはまだ2倍以上の差を付けられている。この差はいったいどこにあるのだ...