QEMUのTCG 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のアドレスを入手できればすぐにジャンプすることで無駄に制御が戻ってしまうことを防ぐ。
これを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 } } }
この方式を採用することで、どれくらい高速化したのかを測定した。
結構速くなったな。最初は4秒近くかかっていたDhrystoneの実行が、2秒程度で終わるまで改善された。なかなか良くなっている。
ただし本家QEMUからはまだ2倍以上の差を付けられている。この差はいったいどこにあるのだ...