diff options
author | Takashi Kokubun <[email protected]> | 2022-12-31 13:41:32 -0800 |
---|---|---|
committer | Takashi Kokubun <[email protected]> | 2023-03-05 22:11:20 -0800 |
commit | c3d99d0f12e2b494e16a5c61134ca312a3a6e3a5 (patch) | |
tree | ad7af15b229a98e599e87668e54bc033f53394a6 /lib/ruby_vm | |
parent | 2b8d1c93ea4d44fba5997901fc4a50b9436bc8b5 (diff) |
Implement defer_compilation
Diffstat (limited to 'lib/ruby_vm')
-rw-r--r-- | lib/ruby_vm/mjit/assembler.rb | 69 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/block_stub.rb | 7 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/code_block.rb | 9 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/compiler.rb | 51 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/context.rb | 9 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/exit_compiler.rb | 61 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/insn_compiler.rb | 42 | ||||
-rw-r--r-- | lib/ruby_vm/mjit/jit_state.rb | 8 |
8 files changed, 207 insertions, 49 deletions
diff --git a/lib/ruby_vm/mjit/assembler.rb b/lib/ruby_vm/mjit/assembler.rb index bfe716e554..bd2da22d69 100644 --- a/lib/ruby_vm/mjit/assembler.rb +++ b/lib/ruby_vm/mjit/assembler.rb @@ -14,9 +14,7 @@ module RubyVM::MJIT Rel32Pad = Object.new # A set of ModR/M values encoded on #insn - class ModRM < Data.define(:mod, :reg, :rm) - def initialize(mod:, reg: nil, rm: nil) = super - end + class ModRM < Data.define(:mod, :reg, :rm); end Mod00 = 0b00 # Mod 00: [reg] Mod01 = 0b01 # Mod 01: [reg]+disp8 Mod10 = 0b10 # Mod 10: [reg]+disp16 @@ -33,9 +31,11 @@ module RubyVM::MJIT @labels = {} @label_id = 0 @comments = Hash.new { |h, k| h[k] = [] } + @stubs = Hash.new { |h, k| h[k] = [] } end def assemble(addr) + set_stub_addrs(addr) resolve_rel32(addr) resolve_labels @@ -67,7 +67,7 @@ module RubyVM::MJIT insn( prefix: REX_W, opcode: 0x83, - mod_rm: ModRM[mod: Mod11, rm: dst_reg], + mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], imm: imm8(src_imm), ) # ADD r/m64, imm8 (Mod 00: [reg]) @@ -77,7 +77,7 @@ module RubyVM::MJIT insn( prefix: REX_W, opcode: 0x83, - mod_rm: ModRM[mod: Mod00, rm: dst_reg], + mod_rm: ModRM[mod: Mod00, reg: 0, rm: dst_reg], imm: imm8(src_imm), ) else @@ -85,12 +85,34 @@ module RubyVM::MJIT end end + # @param addr [Integer] + def call(addr) + # CALL rel32 + # E8 cd + insn(opcode: 0xe8, imm: rel32(addr)) + end + + def jmp(dst) + case dst + # JMP rel32 + in Integer => dst_addr + # E9 cd + insn(opcode: 0xe9, imm: rel32(dst_addr)) + # JMP r/m64 (Mod 11: reg) + in Symbol => dst_reg + # FF /4 + insn(opcode: 0xff, mod_rm: ModRM[mod: Mod11, reg: 4, rm: dst_reg]) + else + raise NotImplementedError, "jmp: not-implemented operands: #{dst.inspect}" + end + end + def jnz(dst) case dst # JNZ rel32 - in Integer => addr + in Integer => dst_addr # 0F 85 cd - insn(opcode: [0x0f, 0x85], imm: rel32(addr)) + insn(opcode: [0x0f, 0x85], imm: rel32(dst_addr)) else raise NotImplementedError, "jnz: not-implemented operands: #{dst.inspect}" end @@ -99,9 +121,9 @@ module RubyVM::MJIT def jz(dst) case dst # JZ rel8 - in Label => label + in Label => dst_label # 74 cb - insn(opcode: 0x74, imm: label) + insn(opcode: 0x74, imm: dst_label) else raise NotImplementedError, "jz: not-implemented operands: #{dst.inspect}" end @@ -155,7 +177,7 @@ module RubyVM::MJIT insn( prefix: REX_W, opcode: 0xc7, - mod_rm: ModRM[mod: Mod11, rm: dst_reg], + mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], imm: imm32(src_imm), ) # MOV r64, imm64 @@ -180,7 +202,7 @@ module RubyVM::MJIT insn( prefix: REX_W, opcode: 0xc7, - mod_rm: ModRM[mod: Mod00, rm: dst_reg], + mod_rm: ModRM[mod: Mod00, reg: 0, rm: dst_reg], imm: imm32(src_imm), ) # MOV r/m64, r64 (Mod 00: [reg]) @@ -207,7 +229,7 @@ module RubyVM::MJIT insn( prefix: REX_W, opcode: 0xc7, - mod_rm: ModRM[mod: Mod01, rm: dst_reg], + mod_rm: ModRM[mod: Mod01, reg: 0, rm: dst_reg], disp: dst_disp, imm: imm32(src_imm), ) @@ -284,6 +306,10 @@ module RubyVM::MJIT @comments[@bytes.size] << message end + def stub(stub) + @stubs[@bytes.size] << stub + end + def new_label(name) Label.new(id: @label_id += 1, name:) end @@ -314,8 +340,8 @@ module RubyVM::MJIT opcode += reg_code(rd) end if mod_rm - prefix |= REX_R if mod_rm.reg && extended_reg?(mod_rm.reg) - prefix |= REX_B if mod_rm.rm && extended_reg?(mod_rm.rm) + prefix |= REX_R if mod_rm.reg.is_a?(Symbol) && extended_reg?(mod_rm.reg) + prefix |= REX_B if mod_rm.rm.is_a?(Symbol) && extended_reg?(mod_rm.rm) end # Encode insn @@ -326,8 +352,8 @@ module RubyVM::MJIT if mod_rm mod_rm_byte = encode_mod_rm( mod: mod_rm.mod, - reg: mod_rm.reg ? reg_code(mod_rm.reg) : 0, - rm: mod_rm.rm ? reg_code(mod_rm.rm) : 0, + reg: mod_rm.reg.is_a?(Symbol) ? reg_code(mod_rm.reg) : mod_rm.reg, + rm: mod_rm.rm.is_a?(Symbol) ? reg_code(mod_rm.rm) : mod_rm.rm, ) @bytes.push(mod_rm_byte) end @@ -413,7 +439,7 @@ module RubyVM::MJIT unless imm32?(imm) raise ArgumentError, "unexpected imm32: #{imm}" end - imm_bytes(imm, 4) + [imm].pack('l').unpack('c*') end # io: 8 bytes @@ -465,6 +491,15 @@ module RubyVM::MJIT [Rel32.new(addr), Rel32Pad, Rel32Pad, Rel32Pad] end + def set_stub_addrs(write_addr) + @bytes.each_with_index do |byte, index| + @stubs.fetch(index, []).each do |stub| + stub.addr = write_addr + index + stub.freeze + end + end + end + def resolve_rel32(write_addr) @bytes.each_with_index do |byte, index| if byte.is_a?(Rel32) diff --git a/lib/ruby_vm/mjit/block_stub.rb b/lib/ruby_vm/mjit/block_stub.rb new file mode 100644 index 0000000000..c6908d09ca --- /dev/null +++ b/lib/ruby_vm/mjit/block_stub.rb @@ -0,0 +1,7 @@ +class RubyVM::MJIT::BlockStub < Struct.new( + :iseq, # @param [RubyVM::MJIT::CPointer::Struct_rb_iseq_struct] Jump target ISEQ + :pc, # @param [Integer] Jump target pc + :ctx, # @param [RubyVM::MJIT::Context] Jump target context + :addr, # @param [Integer] Jump source address to be re-generated +) +end diff --git a/lib/ruby_vm/mjit/code_block.rb b/lib/ruby_vm/mjit/code_block.rb index b4cc3d721c..ad91f53404 100644 --- a/lib/ruby_vm/mjit/code_block.rb +++ b/lib/ruby_vm/mjit/code_block.rb @@ -37,6 +37,15 @@ module RubyVM::MJIT start_addr end + def with_addr(addr) + old_write_pos = @write_pos + @write_pos = addr - @mem_block + @comments.delete(addr) # TODO: clean up old comments for all overwritten insns? + yield + ensure + @write_pos = old_write_pos + end + private def write_addr diff --git a/lib/ruby_vm/mjit/compiler.rb b/lib/ruby_vm/mjit/compiler.rb index ac094c7007..8491ac68c6 100644 --- a/lib/ruby_vm/mjit/compiler.rb +++ b/lib/ruby_vm/mjit/compiler.rb @@ -1,4 +1,5 @@ require 'ruby_vm/mjit/assembler' +require 'ruby_vm/mjit/block_stub' require 'ruby_vm/mjit/code_block' require 'ruby_vm/mjit/context' require 'ruby_vm/mjit/exit_compiler' @@ -38,31 +39,56 @@ module RubyVM::MJIT @insn_compiler = InsnCompiler.new(@ocb) end - # @param iseq [RubyVM::MJIT::CPointer::Struct] - def compile(iseq) + # Compile an ISEQ from its entry point. + # @param iseq `RubyVM::MJIT::CPointer::Struct_rb_iseq_t` + # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` + def compile(iseq, cfp) # TODO: Support has_opt return if iseq.body.param.flags.has_opt asm = Assembler.new - asm.comment("Block: #{iseq.body.location.label}@#{pathobj_path(iseq.body.location.pathobj)}:#{iseq.body.location.first_lineno}") + asm.comment("Block: #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{iseq.body.location.first_lineno}") compile_prologue(asm) - compile_block(asm, iseq) + compile_block(asm, jit: JITState.new(iseq:, cfp:)) iseq.body.jit_func = @cb.write(asm) rescue Exception => e $stderr.puts e.full_message # TODO: check verbose end + # Continue compilation from a stub. + # @param stub [RubyVM::MJIT::BlockStub] + # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` + # @return [Integer] The starting address of a compiled stub + def stub_hit(stub, cfp) + # Update cfp->pc for `jit.at_current_insn?` + cfp.pc = stub.pc + + # Compile the jump target + new_addr = Assembler.new.then do |asm| + jit = JITState.new(iseq: stub.iseq, cfp:) + index = (stub.pc - stub.iseq.body.iseq_encoded.to_i) / C.VALUE.size + compile_block(asm, jit:, index:, ctx: stub.ctx) + @cb.write(asm) + end + + # Re-generate the jump source + @cb.with_addr(stub.addr) do + asm = Assembler.new + asm.comment('regenerate block stub') + asm.jmp(new_addr) + @cb.write(asm) + end + new_addr + end + private - # ec: rdi - # cfp: rsi - # # Callee-saved: rbx, rsp, rbp, r12, r13, r14, r15 # Caller-saved: rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 # # @param asm [RubyVM::MJIT::Assembler] def compile_prologue(asm) - asm.comment("MJIT entry") + asm.comment('MJIT entry') # Save callee-saved registers used by JITed code asm.push(CFP) @@ -78,11 +104,8 @@ module RubyVM::MJIT end # @param asm [RubyVM::MJIT::Assembler] - def compile_block(asm, iseq) - jit = JITState.new - ctx = Context.new - - index = 0 + def compile_block(asm, jit:, index: 0, ctx: Context.new) + iseq = jit.iseq while index < iseq.body.iseq_size insn = self.class.decode_insn(iseq.body.iseq_encoded[index]) jit.pc = (iseq.body.iseq_encoded + index).to_i @@ -181,7 +204,7 @@ module RubyVM::MJIT # opt_mod # opt_eq # opt_neq - # opt_lt + when :opt_lt then @insn_compiler.opt_lt(jit, ctx, asm) # opt_le # opt_gt # opt_ge diff --git a/lib/ruby_vm/mjit/context.rb b/lib/ruby_vm/mjit/context.rb index 2bc499cd4e..8433cc5f95 100644 --- a/lib/ruby_vm/mjit/context.rb +++ b/lib/ruby_vm/mjit/context.rb @@ -1,8 +1,15 @@ class RubyVM::MJIT::Context < Struct.new( - :stack_size, # @param [Integer] + :stack_size, # @param [Integer] The number of values on the stack + :sp_offset, # @param [Integer] JIT sp offset relative to the interpreter's sp ) def initialize(*) super self.stack_size ||= 0 + self.sp_offset ||= 0 + end + + def stack_push(size) + self.stack_size += size + self.sp_offset += size end end diff --git a/lib/ruby_vm/mjit/exit_compiler.rb b/lib/ruby_vm/mjit/exit_compiler.rb index c9f2190931..bec2eab514 100644 --- a/lib/ruby_vm/mjit/exit_compiler.rb +++ b/lib/ruby_vm/mjit/exit_compiler.rb @@ -1,6 +1,9 @@ module RubyVM::MJIT class ExitCompiler - def initialize = freeze + def initialize + # TODO: Use GC offsets + @gc_refs = [] + end # @param jit [RubyVM::MJIT::JITState] # @param ctx [RubyVM::MJIT::Context] @@ -12,19 +15,12 @@ module RubyVM::MJIT asm.mov(:rax, (C.mjit_insn_exits + insn.bin).to_i) asm.add([:rax], 1) # TODO: lock end - asm.comment("exit to interpreter") - # Update pc - asm.mov(:rax, jit.pc) # rax = jit.pc - asm.mov([CFP, C.rb_control_frame_t.offsetof(:pc)], :rax) # cfp->pc = rax - - # Update sp - if ctx.stack_size > 0 - asm.add(SP, C.VALUE.size * ctx.stack_size) # rbx += stack_size - asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], SP) # cfp->sp = rbx - end + # Fix pc/sp offsets for the interpreter + save_pc_and_sp(jit, ctx, asm) # Restore callee-saved registers + asm.comment('exit to interpreter') asm.pop(SP) asm.pop(EC) asm.pop(CFP) @@ -32,5 +28,48 @@ module RubyVM::MJIT asm.mov(:rax, Qundef) asm.ret end + + # @param jit [RubyVM::MJIT::JITState] + # @param asm [RubyVM::MJIT::Assembler] + # @param stub [RubyVM::MJIT::BlockStub] + def compile_jump_stub(jit, asm, stub) + case stub + when BlockStub + asm.comment("block stub hit: #{stub.iseq.body.location.label}@#{C.rb_iseq_path(stub.iseq)}:#{stub.iseq.body.location.first_lineno}") + else + raise "unexpected stub object: #{stub.inspect}" + end + + # Call rb_mjit_stub_hit + asm.mov(:rdi, to_value(stub)) + asm.call(C.rb_mjit_stub_hit) + + # Jump to the address returned by rb_mjit_stub_hit + asm.jmp(:rax) + end + + private + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def save_pc_and_sp(jit, ctx, asm) + # Update pc + asm.comment("save pc #{'and sp' if ctx.sp_offset != 0}") + asm.mov(:rax, jit.pc) # rax = jit.pc + asm.mov([CFP, C.rb_control_frame_t.offsetof(:pc)], :rax) # cfp->pc = rax + + # Update sp + if ctx.sp_offset != 0 + asm.add(SP, C.VALUE.size * ctx.sp_offset) # sp += stack_size + asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], SP) # cfp->sp = sp + ctx.sp_offset = 0 + end + end + + def to_value(obj) + @gc_refs << obj + C.to_value(obj) + end end end diff --git a/lib/ruby_vm/mjit/insn_compiler.rb b/lib/ruby_vm/mjit/insn_compiler.rb index 96bab0a225..0d40cb6f50 100644 --- a/lib/ruby_vm/mjit/insn_compiler.rb +++ b/lib/ruby_vm/mjit/insn_compiler.rb @@ -1,7 +1,7 @@ module RubyVM::MJIT # scratch regs: rax # - # 4/101 + # 5/101 class InsnCompiler # @param ocb [CodeBlock] def initialize(ocb) @@ -33,7 +33,7 @@ module RubyVM::MJIT # @param asm [RubyVM::MJIT::Assembler] def putnil(jit, ctx, asm) asm.mov([SP, C.VALUE.size * ctx.stack_size], Qnil) - ctx.stack_size += 1 + ctx.stack_push(1) KeepCompiling end @@ -55,7 +55,7 @@ module RubyVM::MJIT asm.mov([SP, C.VALUE.size * ctx.stack_size], :rax) end - ctx.stack_size += 1 + ctx.stack_push(1) KeepCompiling end @@ -141,7 +141,18 @@ module RubyVM::MJIT # opt_mod # opt_eq # opt_neq - # opt_lt + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_lt(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + CantCompile + end + # opt_le # opt_gt # opt_ge @@ -178,7 +189,7 @@ module RubyVM::MJIT # Push it to the stack asm.mov([SP, C.VALUE.size * ctx.stack_size], :rax) - ctx.stack_size += 1 + ctx.stack_push(1) KeepCompiling end @@ -198,6 +209,27 @@ module RubyVM::MJIT # @param jit [RubyVM::MJIT::JITState] # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def defer_compilation(jit, ctx, asm) + # Make a stub to compile the current insn + block_stub = BlockStub.new( + iseq: jit.iseq, + pc: jit.pc, + ctx: ctx.dup, + ) + + stub_hit = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_jump_stub(jit, ocb_asm, block_stub) + @ocb.write(ocb_asm) + end + + asm.comment('defer_compilation: block stub') + asm.stub(block_stub) + asm.jmp(stub_hit) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] def compile_side_exit(jit, ctx) asm = Assembler.new @exit_compiler.compile_exit(jit, ctx, asm) diff --git a/lib/ruby_vm/mjit/jit_state.rb b/lib/ruby_vm/mjit/jit_state.rb index 0a35a516ca..1a3e78b3da 100644 --- a/lib/ruby_vm/mjit/jit_state.rb +++ b/lib/ruby_vm/mjit/jit_state.rb @@ -1,9 +1,15 @@ module RubyVM::MJIT class JITState < Struct.new( - :pc, # @param [Integer] + :iseq, + :pc, # @param [Integer] The JIT target PC + :cfp, # @param `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` The JIT source CFP (before MJIT is called) ) def operand(index) C.VALUE.new(pc)[index + 1] end + + def at_current_insn? + pc == cfp.pc.to_i + end end end |