diff options
Diffstat (limited to 'lib/ruby_vm')
| -rw-r--r-- | lib/ruby_vm/mjit/assembler.rb | 714 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/block.rb | 6 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/branch_stub.rb | 24 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/c_pointer.rb | 343 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/c_type.rb | 91 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/code_block.rb | 86 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/compiler.rb | 188 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/context.rb | 21 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/exit_compiler.rb | 119 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/hooks.rb | 32 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/insn_compiler.rb | 857 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/invariants.rb | 62 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/jit_state.rb | 30 | ||||
| -rw-r--r-- | lib/ruby_vm/mjit/stats.rb | 87 |
14 files changed, 0 insertions, 2660 deletions
diff --git a/lib/ruby_vm/mjit/assembler.rb b/lib/ruby_vm/mjit/assembler.rb deleted file mode 100644 index f2f5bd77c4..0000000000 --- a/lib/ruby_vm/mjit/assembler.rb +++ /dev/null @@ -1,714 +0,0 @@ -# frozen_string_literal: true -module RubyVM::MJIT - # https://www.intel.com/content/dam/develop/public/us/en/documents/325383-sdm-vol-2abcd.pdf - # Mostly an x86_64 assembler, but this also has some stuff that is useful for any architecture. - class Assembler - # rel8 jumps are made with labels - class Label < Data.define(:id, :name); end - - # rel32 is inserted as [Rel32, Rel32Pad..] and converted on #resolve_rel32 - class Rel32 < Data.define(:addr); end - Rel32Pad = Object.new - - # A set of ModR/M values encoded on #insn - class ModRM < Data.define(:mod, :reg, :rm); end - Mod00 = 0b00 # Mod 00: [reg] - Mod01 = 0b01 # Mod 01: [reg]+disp8 - Mod10 = 0b10 # Mod 10: [reg]+disp32 - Mod11 = 0b11 # Mod 11: reg - - # REX = 0100WR0B - REX_B = 0b01000001 - REX_R = 0b01000100 - REX_W = 0b01001000 - - def initialize - @bytes = [] - @labels = {} - @label_id = 0 - @comments = Hash.new { |h, k| h[k] = [] } - @blocks = Hash.new { |h, k| h[k] = [] } - @stub_starts = Hash.new { |h, k| h[k] = [] } - @stub_ends = Hash.new { |h, k| h[k] = [] } - end - - def assemble(addr) - set_code_addrs(addr) - resolve_rel32(addr) - resolve_labels - - write_bytes(addr) - - @bytes.size - ensure - @bytes.clear - end - - def size - @bytes.size - end - - # - # Instructions - # - - def add(dst, src) - case [dst, src] - # ADD r/m64, imm8 (Mod 00: [reg]) - in [[Symbol => dst_reg], Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) - # REX.W + 83 /0 ib - # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 - insn( - prefix: REX_W, - opcode: 0x83, - mod_rm: ModRM[mod: Mod00, reg: 0, rm: dst_reg], - imm: imm8(src_imm), - ) - # ADD r/m64, imm8 (Mod 11: reg) - in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) - # REX.W + 83 /0 ib - # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 - insn( - prefix: REX_W, - opcode: 0x83, - mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], - imm: imm8(src_imm), - ) - # ADD r/m64 imm32 (Mod 11: reg) - in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm32?(src_imm) - # REX.W + 81 /0 id - # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 - insn( - prefix: REX_W, - opcode: 0x81, - mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], - imm: imm32(src_imm), - ) - # ADD r/m64, r64 (Mod 11: reg) - in [Symbol => dst_reg, Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) - # REX.W + 01 /r - # MR: Operand 1: ModRM:r/m (r, w), Operand 2: ModRM:reg (r) - insn( - prefix: REX_W, - opcode: 0x01, - mod_rm: ModRM[mod: Mod11, reg: src_reg, rm: dst_reg], - ) - else - raise NotImplementedError, "add: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - end - - # @param addr [Integer] - def call(addr) - # CALL rel32 - # E8 cd - insn(opcode: 0xe8, imm: rel32(addr)) - end - - def cmovl(dst, src) - case [dst, src] - # CMOVL r64, r/m64 (Mod 11: reg) - in [Symbol => dst_reg, Symbol => src_reg] - # REX.W + 0F 4C /r - # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) - insn( - prefix: REX_W, - opcode: [0x0f, 0x4c], - mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], - ) - else - raise NotImplementedError, "cmovl: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - end - - def cmp(left, right) - case [left, right] - # CMP r/m64 r64 (Mod 01: [reg]+disp8) - in [[Symbol => left_reg, Integer => left_disp], Symbol => right_reg] - # REX.W + 39 /r - # MR: Operand 1: ModRM:r/m (r), Operand 2: ModRM:reg (r) - insn( - prefix: REX_W, - opcode: 0x39, - mod_rm: ModRM[mod: Mod01, reg: right_reg, rm: left_reg], - disp: left_disp, - ) - else - raise NotImplementedError, "cmp: not-implemented operands: #{left.inspect}, #{right.inspect}" - end - end - - def je(dst) - case dst - # JE rel32 - in Integer => dst_addr - # 0F 84 cd - insn(opcode: [0x0f, 0x84], imm: rel32(dst_addr)) - else - raise NotImplementedError, "je: not-implemented operands: #{dst.inspect}" - end - 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 01: [reg]+disp8) - in [Symbol => dst_reg, Integer => dst_disp] if imm8?(dst_disp) - # FF /4 - insn(opcode: 0xff, mod_rm: ModRM[mod: Mod01, reg: 4, rm: dst_reg], disp: dst_disp) - # 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 jne(dst) - case dst - # JNE rel32 - in Integer => dst_addr - # 0F 85 cd - insn(opcode: [0x0f, 0x85], imm: rel32(dst_addr)) - else - raise NotImplementedError, "jne: not-implemented operands: #{dst.inspect}" - end - end - - def jnz(dst) - case dst - # JNZ rel32 - in Integer => dst_addr - # 0F 85 cd - insn(opcode: [0x0f, 0x85], imm: rel32(dst_addr)) - else - raise NotImplementedError, "jnz: not-implemented operands: #{dst.inspect}" - end - end - - def jo(dst) - case dst - # JO rel32 - in Integer => dst_addr - # 0F 80 cd - insn(opcode: [0x0f, 0x80], imm: rel32(dst_addr)) - else - raise NotImplementedError, "jo: not-implemented operands: #{dst.inspect}" - end - end - - def jz(dst) - case dst - # JZ rel8 - in Label => dst_label - # 74 cb - insn(opcode: 0x74, imm: dst_label) - # JZ rel32 - in Integer => dst_addr - # 0F 84 cd - insn(opcode: [0x0f, 0x84], imm: rel32(dst_addr)) - else - raise NotImplementedError, "jz: not-implemented operands: #{dst.inspect}" - end - end - - def lea(dst, src) - case [dst, src] - # LEA r64,m (Mod 01: [reg]+disp8) - in [Symbol => dst_reg, [Symbol => src_reg, Integer => src_disp]] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) - # REX.W + 8D /r - # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) - insn( - prefix: REX_W, - opcode: 0x8d, - mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], - disp: src_disp, - ) - else - raise NotImplementedError, "lea: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - end - - def mov(dst, src) - case dst - in Symbol => dst_reg - case src - # MOV r64, r/m64 (Mod 00: [reg]) - in [Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) - # REX.W + 8B /r - # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) - insn( - prefix: REX_W, - opcode: 0x8b, - mod_rm: ModRM[mod: Mod00, reg: dst_reg, rm: src_reg], - ) - # MOV r64, r/m64 (Mod 01: [reg]+disp8) - in [Symbol => src_reg, Integer => src_disp] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) - # REX.W + 8B /r - # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) - insn( - prefix: REX_W, - opcode: 0x8b, - mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], - disp: src_disp, - ) - # MOV r64, r/m64 (Mod 10: [reg]+disp16) - in [Symbol => src_reg, Integer => src_disp] if r64?(dst_reg) && r64?(src_reg) && imm32?(src_disp) - # REX.W + 8B /r - # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) - insn( - prefix: REX_W, - opcode: 0x8b, - mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg], - disp: imm32(src_disp), - ) - # MOV r64, r/m64 (Mod 11: reg) - in Symbol => src_reg if r64?(dst_reg) && r64?(src_reg) - # REX.W + 8B /r - # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) - insn( - prefix: REX_W, - opcode: 0x8b, - mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], - ) - # MOV r32 r/m32 (Mod 01: [reg]+disp8) - in [Symbol => src_reg, Integer => src_disp] if r32?(dst_reg) && imm8?(src_disp) - # 8B /r - # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) - insn( - opcode: 0x8b, - mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], - disp: src_disp, - ) - # MOV r32, imm32 (Mod 11: reg) - in Integer => src_imm if r32?(dst_reg) && imm32?(src_imm) - # B8+ rd id - # OI: Operand 1: opcode + rd (w), Operand 2: imm8/16/32/64 - insn( - opcode: 0xb8, - rd: dst_reg, - imm: imm32(src_imm), - ) - # MOV r/m64, imm32 (Mod 11: reg) - in Integer => src_imm if r64?(dst_reg) && imm32?(src_imm) - # REX.W + C7 /0 id - # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 - insn( - prefix: REX_W, - opcode: 0xc7, - mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], - imm: imm32(src_imm), - ) - # MOV r64, imm64 - in Integer => src_imm if r64?(dst_reg) && imm64?(src_imm) - # REX.W + B8+ rd io - # OI: Operand 1: opcode + rd (w), Operand 2: imm8/16/32/64 - insn( - prefix: REX_W, - opcode: 0xb8, - rd: dst_reg, - imm: imm64(src_imm), - ) - else - raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - in [Symbol => dst_reg] - case src - # MOV r/m64, imm32 (Mod 00: [reg]) - in Integer => src_imm if r64?(dst_reg) && imm32?(src_imm) - # REX.W + C7 /0 id - # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 - insn( - prefix: REX_W, - opcode: 0xc7, - mod_rm: ModRM[mod: Mod00, reg: 0, rm: dst_reg], - imm: imm32(src_imm), - ) - # MOV r/m64, r64 (Mod 00: [reg]) - in Symbol => src_reg if r64?(dst_reg) && r64?(src_reg) - # REX.W + 89 /r - # MR: Operand 1: ModRM:r/m (w), Operand 2: ModRM:reg (r) - insn( - prefix: REX_W, - opcode: 0x89, - mod_rm: ModRM[mod: Mod00, reg: src_reg, rm: dst_reg], - ) - else - raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - in [Symbol => dst_reg, Integer => dst_disp] - # Optimize encoding when disp is 0 - return mov([dst_reg], src) if dst_disp == 0 - - case src - # MOV r/m64, imm32 (Mod 01: [reg]+disp8) - in Integer => src_imm if r64?(dst_reg) && imm8?(dst_disp) && imm32?(src_imm) - # REX.W + C7 /0 id - # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 - insn( - prefix: REX_W, - opcode: 0xc7, - mod_rm: ModRM[mod: Mod01, reg: 0, rm: dst_reg], - disp: dst_disp, - imm: imm32(src_imm), - ) - # MOV r/m64, r64 (Mod 01: [reg]+disp8) - in Symbol => src_reg if r64?(dst_reg) && imm8?(dst_disp) && r64?(src_reg) - # REX.W + 89 /r - # MR: Operand 1: ModRM:r/m (w), Operand 2: ModRM:reg (r) - insn( - prefix: REX_W, - opcode: 0x89, - mod_rm: ModRM[mod: Mod01, reg: src_reg, rm: dst_reg], - disp: dst_disp, - ) - else - raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - else - raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - end - - def push(src) - case src - # PUSH r64 - in Symbol => src_reg if r64?(src_reg) - # 50+rd - # O: Operand 1: opcode + rd (r) - insn(opcode: 0x50, rd: src_reg) - else - raise NotImplementedError, "push: not-implemented operands: #{src.inspect}" - end - end - - def pop(dst) - case dst - # POP r64 - in Symbol => dst_reg if r64?(dst_reg) - # 58+ rd - # O: Operand 1: opcode + rd (r) - insn(opcode: 0x58, rd: dst_reg) - else - raise NotImplementedError, "pop: not-implemented operands: #{dst.inspect}" - end - end - - def ret - # RET - # Near return: A return to a procedure within the current code segment - insn(opcode: 0xc3) - end - - def sub(dst, src) - case [dst, src] - # SUB r/m64, imm8 - in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) - # REX.W + 83 /5 ib - # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 - insn( - prefix: REX_W, - opcode: 0x83, - mod_rm: ModRM[mod: Mod11, reg: 5, rm: dst_reg], - imm: imm8(src_imm), - ) - # SUB r/m64, r64 (Mod 11: reg) - in [Symbol => dst_reg, Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) - # REX.W + 29 /r - # MR: Operand 1: ModRM:r/m (r, w), Operand 2: ModRM:reg (r) - insn( - prefix: REX_W, - opcode: 0x29, - mod_rm: ModRM[mod: Mod11, reg: src_reg, rm: dst_reg], - ) - else - raise NotImplementedError, "sub: not-implemented operands: #{dst.inspect}, #{src.inspect}" - end - end - - def test(left, right) - case [left, right] - # TEST r/m8*, imm8 (Mod 01: [reg]+disp8) - in [[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if imm8?(right_imm) && right_imm >= 0 - # REX + F6 /0 ib - # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 - insn( - opcode: 0xf6, - mod_rm: ModRM[mod: Mod01, reg: 0, rm: left_reg], - disp: left_disp, - imm: imm8(right_imm), - ) - # TEST r/m64, imm32 (Mod 01: [reg]+disp8) - in [[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if imm32?(right_imm) - # REX.W + F7 /0 id - # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 - insn( - prefix: REX_W, - opcode: 0xf7, - mod_rm: ModRM[mod: Mod01, reg: 0, rm: left_reg], - disp: left_disp, - imm: imm32(right_imm), - ) - # TEST r/m32, r32 (Mod 11: reg) - in [Symbol => left_reg, Symbol => right_reg] if r32?(left_reg) && r32?(right_reg) - # 85 /r - # MR: Operand 1: ModRM:r/m (r), Operand 2: ModRM:reg (r) - insn( - opcode: 0x85, - mod_rm: ModRM[mod: Mod11, reg: right_reg, rm: left_reg], - ) - else - raise NotImplementedError, "test: not-implemented operands: #{left.inspect}, #{right.inspect}" - end - end - - # - # Utilities - # - - attr_reader :comments - - def comment(message) - @comments[@bytes.size] << message - end - - # Mark the starting address of a block - def block(block) - @blocks[@bytes.size] << block - end - - # Mark the starting/ending addresses of a stub - def stub(stub) - @stub_starts[@bytes.size] << stub - yield - ensure - @stub_ends[@bytes.size] << stub - end - - def new_label(name) - Label.new(id: @label_id += 1, name:) - end - - # @param [RubyVM::MJIT::Assembler::Label] label - def write_label(label) - @labels[label] = @bytes.size - end - - def incr_counter(name) - if C.mjit_opts.stats - comment("increment counter #{name}") - mov(:rax, C.rb_mjit_counters[name].to_i) - add([:rax], 1) # TODO: lock - end - end - - def imm32?(imm) - (-0x8000_0000..0x7fff_ffff).include?(imm) # TODO: consider uimm - end - - private - - def insn(prefix: 0, opcode:, rd: nil, mod_rm: nil, disp: nil, imm: nil) - # Determine prefix - if rd - prefix |= REX_B if extended_reg?(rd) - opcode += reg_code(rd) - end - if mod_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 - if prefix > 0 - @bytes.push(prefix) - end - @bytes.push(*Array(opcode)) - if mod_rm - mod_rm_byte = encode_mod_rm( - mod: mod_rm.mod, - 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 - if disp - @bytes.push(*Array(disp)) - end - if imm - @bytes.push(*imm) - end - end - - def reg_code(reg) - reg_code_extended(reg).first - end - - def extended_reg?(reg) - reg_code_extended(reg).last - end - - def reg_code_extended(reg) - case reg - # Not extended - when :al, :ax, :eax, :rax then [0, false] - when :cl, :cx, :ecx, :rcx then [1, false] - when :dl, :dx, :edx, :rdx then [2, false] - when :bl, :bx, :ebx, :rbx then [3, false] - when :ah, :sp, :esp, :rsp then [4, false] - when :ch, :bp, :ebp, :rbp then [5, false] - when :dh, :si, :esi, :rsi then [6, false] - when :bh, :di, :edi, :rdi then [7, false] - # Extended - when :r8b, :r8w, :r8d, :r8 then [0, true] - when :r9b, :r9w, :r9d, :r9 then [1, true] - when :r10b, :r10w, :r10d, :r10 then [2, true] - when :r11b, :r11w, :r11d, :r11 then [3, true] - when :r12b, :r12w, :r12d, :r12 then [4, true] - when :r13b, :r13w, :r13d, :r13 then [5, true] - when :r14b, :r14w, :r14d, :r14 then [6, true] - when :r15b, :r15w, :r15d, :r15 then [7, true] - else raise ArgumentError, "unexpected reg: #{reg.inspect}" - end - end - - # Table 2-2. 32-Bit Addressing Forms with the ModR/M Byte - # - # 7 6 5 4 3 2 1 0 - # +--+--+--+--+--+--+--+--+ - # | Mod | Reg/ | R/M | - # | | Opcode | | - # +--+--+--+--+--+--+--+--+ - # - # The r/m field can specify a register as an operand or it can be combined - # with the mod field to encode an addressing mode. - # - # /0: R/M is 0 (not used) - # /r: R/M is a register - def encode_mod_rm(mod:, reg: 0, rm: 0) - if mod > 0b11 - raise ArgumentError, "too large Mod: #{mod}" - end - if reg > 0b111 - raise ArgumentError, "too large Reg/Opcode: #{reg}" - end - if rm > 0b111 - raise ArgumentError, "too large R/M: #{rm}" - end - (mod << 6) + (reg << 3) + rm - end - - # ib: 1 byte - def imm8(imm) - unless imm8?(imm) - raise ArgumentError, "unexpected imm8: #{imm}" - end - [imm].pack('c').unpack('c*') # TODO: consider uimm - end - - # id: 4 bytes - def imm32(imm) - unless imm32?(imm) - raise ArgumentError, "unexpected imm32: #{imm}" - end - [imm].pack('l').unpack('c*') # TODO: consider uimm - end - - # io: 8 bytes - def imm64(imm) - unless imm64?(imm) - raise ArgumentError, "unexpected imm64: #{imm}" - end - imm_bytes(imm, 8) - end - - def imm_bytes(imm, num_bytes) - bytes = [] - bits = imm - num_bytes.times do - bytes << (bits & 0xff) - bits >>= 8 - end - if bits != 0 - raise ArgumentError, "unexpected imm with #{num_bytes} bytes: #{imm}" - end - bytes - end - - def imm8?(imm) - (-0x80..0x7f).include?(imm) - end - - def imm64?(imm) - (-0x8000_0000_0000_0000..0xffff_ffff_ffff_ffff).include?(imm) - end - - def r32?(reg) - if extended_reg?(reg) - reg.end_with?('d') - else - reg.start_with?('e') - end - end - - def r64?(reg) - if extended_reg?(reg) - reg.match?(/\Ar\d+\z/) - else - reg.start_with?('r') - end - end - - def rel32(addr) - [Rel32.new(addr), Rel32Pad, Rel32Pad, Rel32Pad] - end - - def set_code_addrs(write_addr) - (@bytes.size + 1).times do |index| - @blocks.fetch(index, []).each do |block| - block.start_addr = write_addr + index - end - @stub_starts.fetch(index, []).each do |stub| - stub.start_addr = write_addr + index - end - @stub_ends.fetch(index, []).each do |stub| - stub.end_addr = write_addr + index - end - end - end - - def resolve_rel32(write_addr) - @bytes.each_with_index do |byte, index| - if byte.is_a?(Rel32) - src_addr = write_addr + index + 4 # offset 4 bytes for rel32 itself - dst_addr = byte.addr - rel32 = dst_addr - src_addr - raise "unexpected offset: #{rel32}" unless imm32?(rel32) - imm32(rel32).each_with_index do |rel_byte, rel_index| - @bytes[index + rel_index] = rel_byte - end - end - end - end - - def resolve_labels - @bytes.each_with_index do |byte, index| - if byte.is_a?(Label) - src_index = index + 1 # offset 1 byte for rel8 itself - dst_index = @labels.fetch(byte) - rel8 = dst_index - src_index - raise "unexpected offset: #{rel8}" unless imm8?(rel8) - @bytes[index] = rel8 - end - end - end - - def write_bytes(addr) - Fiddle::Pointer.new(addr)[0, @bytes.size] = @bytes.pack('c*') - end - end -end diff --git a/lib/ruby_vm/mjit/block.rb b/lib/ruby_vm/mjit/block.rb deleted file mode 100644 index 58c21c9c2a..0000000000 --- a/lib/ruby_vm/mjit/block.rb +++ /dev/null @@ -1,6 +0,0 @@ -class RubyVM::MJIT::Block < Struct.new( - :pc, # @param [Integer] Starting PC - :start_addr, # @param [Integer] Starting address of this block's JIT code - :entry_exit, # @param [Integer] Address of entry exit (optional) -) -end diff --git a/lib/ruby_vm/mjit/branch_stub.rb b/lib/ruby_vm/mjit/branch_stub.rb deleted file mode 100644 index 0f015e2f72..0000000000 --- a/lib/ruby_vm/mjit/branch_stub.rb +++ /dev/null @@ -1,24 +0,0 @@ -module RubyVM::MJIT - # Branch shapes - Next0 = :Next0 # target0 is a fallthrough - Next1 = :Next1 # target1 is a fallthrough - Default = :Default # neither targets is a fallthrough - - class BranchStub < Struct.new( - :iseq, # @param [RubyVM::MJIT::CPointer::Struct_rb_iseq_struct] Branch target ISEQ - :shape, # @param [Symbol] Next0, Next1, or Default - :target0, # @param [RubyVM::MJIT::BranchTarget] First branch target - :target1, # @param [RubyVM::MJIT::BranchTarget,NilClass] Second branch target (optional) - :compile, # @param [Proc] A callback to (re-)generate this branch stub - :start_addr, # @param [Integer] Stub source start address to be re-generated - :end_addr, # @param [Integer] Stub source end address to be re-generated - ) - end - - class BranchTarget < Struct.new( - :pc, - :ctx, - :address, - ) - end -end diff --git a/lib/ruby_vm/mjit/c_pointer.rb b/lib/ruby_vm/mjit/c_pointer.rb deleted file mode 100644 index 03742dd53a..0000000000 --- a/lib/ruby_vm/mjit/c_pointer.rb +++ /dev/null @@ -1,343 +0,0 @@ -module RubyVM::MJIT - # Every class under this namespace is a pointer. Even if the type is - # immediate, it shouldn't be dereferenced until `*` is called. - module CPointer - # Note: We'd like to avoid alphabetic method names to avoid a conflict - # with member methods. to_i and to_s are considered an exception. - class Struct - # @param name [String] - # @param sizeof [Integer] - # @param members [Hash{ Symbol => [RubyVM::MJIT::CType::*, Integer, TrueClass] }] - def initialize(addr, sizeof, members) - @addr = addr - @sizeof = sizeof - @members = members - end - - # Get a raw address - def to_i - @addr - end - - # Serialized address for generated code - def to_s - "0x#{@addr.to_s(16)}" - end - - # Pointer diff - def -(struct) - raise ArgumentError if self.class != struct.class - (@addr - struct.to_i) / @sizeof - end - - # Primitive API that does no automatic dereference - # TODO: remove this? - # @param member [Symbol] - def [](member) - type, offset = @members.fetch(member) - type.new(@addr + offset / 8) - end - - private - - # @param member [Symbol] - # @param value [Object] - def []=(member, value) - type, offset = @members.fetch(member) - type[@addr + offset / 8] = value - end - - # @param size [Integer] - # @param members [Hash{ Symbol => [Integer, RubyVM::MJIT::CType::*] }] - def self.define(size, members) - Class.new(self) do - # Return the size of this type - define_singleton_method(:size) { size } - - # Return the offset to a field - define_singleton_method(:offsetof) { |field| members.fetch(field).last / 8 } - - # Return member names - define_singleton_method(:members) { members.keys } - - define_method(:initialize) do |addr = nil| - if addr.nil? # TODO: get rid of this feature later - addr = Fiddle.malloc(size) - end - super(addr, size, members) - end - - members.each do |member, (type, offset, to_ruby)| - # Intelligent API that does automatic dereference - define_method(member) do - value = self[member] - if value.respond_to?(:*) - value = value.* - end - if to_ruby - value = C.to_ruby(value) - end - value - end - - define_method("#{member}=") do |value| - if to_ruby - value = C.to_value(value) - end - self[member] = value - end - end - end - end - end - - # Note: We'd like to avoid alphabetic method names to avoid a conflict - # with member methods. to_i is considered an exception. - class Union - # @param _name [String] To be used when it starts defining a union pointer class - # @param sizeof [Integer] - # @param members [Hash{ Symbol => RubyVM::MJIT::CType::* }] - def initialize(addr, sizeof, members) - @addr = addr - @sizeof = sizeof - @members = members - end - - # Get a raw address - def to_i - @addr - end - - # Move addr to access this pointer like an array - def +(index) - raise ArgumentError unless index.is_a?(Integer) - self.class.new(@addr + index * @sizeof) - end - - # Pointer diff - def -(union) - raise ArgumentError if self.class != union.class - (@addr - union.instance_variable_get(:@addr)) / @sizeof - end - - # @param sizeof [Integer] - # @param members [Hash{ Symbol => RubyVM::MJIT::CType::* }] - def self.define(sizeof, members) - Class.new(self) do - # Return the size of this type - define_singleton_method(:sizeof) { sizeof } - - define_method(:initialize) do |addr| - super(addr, sizeof, members) - end - - members.each do |member, type| - # Intelligent API that does automatic dereference - define_method(member) do - value = type.new(@addr) - if value.respond_to?(:*) - value = value.* - end - value - end - end - end - end - end - - class Immediate - # @param addr [Integer] - # @param size [Integer] - # @param pack [String] - def initialize(addr, size, pack) - @addr = addr - @size = size - @pack = pack - end - - # Get a raw address - def to_i - @addr - end - - # Move addr to addess this pointer like an array - def +(index) - Immediate.new(@addr + index * @size, @size, @pack) - end - - # Dereference - def * - self[0] - end - - # Array access - def [](index) - return nil if @addr == 0 - Fiddle::Pointer.new(@addr + index * @size)[0, @size].unpack1(@pack) - end - - # Array set - def []=(index, value) - Fiddle::Pointer.new(@addr + index * @size)[0, @size] = [value].pack(@pack) - end - - # Serialized address for generated code. Used for embedding things like body->iseq_encoded. - def to_s - "0x#{Integer(@addr).to_s(16)}" - end - - # @param fiddle_type [Integer] Fiddle::TYPE_* - def self.define(fiddle_type) - size = Fiddle::PackInfo::SIZE_MAP.fetch(fiddle_type) - pack = Fiddle::PackInfo::PACK_MAP.fetch(fiddle_type) - - Class.new(self) do - define_method(:initialize) do |addr| - super(addr, size, pack) - end - - define_singleton_method(:size) do - size - end - - # Type-level []=: Used by struct fields - define_singleton_method(:[]=) do |addr, value| - Fiddle::Pointer.new(addr)[0, size] = [value].pack(pack) - end - end - end - end - - # -Fiddle::TYPE_CHAR Immediate with special handling of true/false - class Bool < Immediate.define(-Fiddle::TYPE_CHAR) - # Dereference - def * - return nil if @addr == 0 - super != 0 - end - - def self.[]=(addr, value) - super(addr, value ? 1 : 0) - end - end - - class Pointer - attr_reader :type - - # @param addr [Integer] - # @param type [Class] RubyVM::MJIT::CType::* - def initialize(addr, type) - @addr = addr - @type = type - end - - # Move addr to addess this pointer like an array - def +(index) - raise ArgumentError unless index.is_a?(Integer) - Pointer.new(@addr + index * Fiddle::SIZEOF_VOIDP, @type) - end - - # Dereference - def * - return nil if dest_addr == 0 - @type.new(dest_addr) - end - - # Array access - def [](index) - (self + index).* - end - - # Array set - # @param index [Integer] - # @param value [Integer, RubyVM::MJIT::CPointer::Struct] an address itself or an object that return an address with to_i - def []=(index, value) - Fiddle::Pointer.new(@addr + index * Fiddle::SIZEOF_VOIDP)[0, Fiddle::SIZEOF_VOIDP] = - [value.to_i].pack(Fiddle::PackInfo::PACK_MAP[Fiddle::TYPE_VOIDP]) - end - - # Get a raw address - def to_i - @addr - end - - private - - def dest_addr - Fiddle::Pointer.new(@addr)[0, Fiddle::SIZEOF_VOIDP].unpack1(Fiddle::PackInfo::PACK_MAP[Fiddle::TYPE_VOIDP]) - end - - def self.define(block) - Class.new(self) do - define_method(:initialize) do |addr| - super(addr, block.call) - end - - # Type-level []=: Used by struct fields - # @param addr [Integer] - # @param value [Integer, RubyVM::MJIT::CPointer::Struct] an address itself, or an object that return an address with to_i - define_singleton_method(:[]=) do |addr, value| - value = value.to_i - Fiddle::Pointer.new(addr)[0, Fiddle::SIZEOF_VOIDP] = [value].pack(Fiddle::PackInfo::PACK_MAP[Fiddle::TYPE_VOIDP]) - end - end - end - end - - class BitField - # @param addr [Integer] - # @param width [Integer] - # @param offset [Integer] - def initialize(addr, width, offset) - @addr = addr - @width = width - @offset = offset - end - - # Dereference - def * - byte = Fiddle::Pointer.new(@addr)[0, Fiddle::SIZEOF_CHAR].unpack('c').first - if @width == 1 - bit = (1 & (byte >> @offset)) - bit == 1 - elsif @width <= 8 && @offset == 0 - bitmask = @width.times.map { |i| 1 << i }.sum - byte & bitmask - else - raise NotImplementedError.new("not-implemented bit field access: width=#{@width} offset=#{@offset}") - end - end - - # @param width [Integer] - # @param offset [Integer] - def self.define(width, offset) - Class.new(self) do - define_method(:initialize) do |addr| - super(addr, width, offset) - end - end - end - end - - # Give a name to a dynamic CPointer class to see it on inspect - def self.with_class_name(prefix, name, cache: false, &block) - return block.call if name.empty? - - # Use a cached result only if cache: true - class_name = "#{prefix}_#{name}" - klass = - if cache && self.const_defined?(class_name) - self.const_get(class_name) - else - block.call - end - - # Give it a name unless it's already defined - unless self.const_defined?(class_name) - self.const_set(class_name, klass) - end - - klass - end - end -end diff --git a/lib/ruby_vm/mjit/c_type.rb b/lib/ruby_vm/mjit/c_type.rb deleted file mode 100644 index 9e45d8d41c..0000000000 --- a/lib/ruby_vm/mjit/c_type.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'fiddle' -require 'fiddle/pack' -require_relative 'c_pointer' - -module RubyVM::MJIT - module CType - module Struct - # @param name [String] - # @param members [Hash{ Symbol => [Integer, RubyVM::MJIT::CType::*] }] - def self.new(name, sizeof, **members) - name = members.keys.join('_') if name.empty? - CPointer.with_class_name('Struct', name) do - CPointer::Struct.define(sizeof, members) - end - end - end - - module Union - # @param name [String] - # @param members [Hash{ Symbol => RubyVM::MJIT::CType::* }] - def self.new(name, sizeof, **members) - name = members.keys.join('_') if name.empty? - CPointer.with_class_name('Union', name) do - CPointer::Union.define(sizeof, members) - end - end - end - - module Immediate - # @param fiddle_type [Integer] - def self.new(fiddle_type) - name = Fiddle.constants.find do |const| - const.start_with?('TYPE_') && Fiddle.const_get(const) == fiddle_type.abs - end&.to_s - name.delete_prefix!('TYPE_') - if fiddle_type.negative? - name.prepend('U') - end - CPointer.with_class_name('Immediate', name, cache: true) do - CPointer::Immediate.define(fiddle_type) - end - end - - # @param type [String] - def self.parse(ctype) - new(Fiddle::Importer.parse_ctype(ctype)) - end - - def self.find(size, signed) - fiddle_type = TYPE_MAP.fetch(size) - fiddle_type = -fiddle_type unless signed - new(fiddle_type) - end - - TYPE_MAP = Fiddle::PackInfo::SIZE_MAP.map { |type, size| [size, type.abs] }.to_h - private_constant :TYPE_MAP - end - - module Bool - def self.new - CPointer::Bool - end - end - - class Pointer - # This takes a block to avoid "stack level too deep" on a cyclic reference - # @param block [Proc] - def self.new(&block) - CPointer.with_class_name('Pointer', block.object_id.to_s) do - CPointer::Pointer.define(block) - end - end - end - - module BitField - # @param width [Integer] - # @param offset [Integer] - def self.new(width, offset) - CPointer.with_class_name('BitField', "#{offset}_#{width}") do - CPointer::BitField.define(width, offset) - end - end - end - - # Types that are referenced but not part of code generation targets - Stub = ::Struct.new(:name) - - # Types that it failed to figure out from the header - Unknown = Module.new - end -end diff --git a/lib/ruby_vm/mjit/code_block.rb b/lib/ruby_vm/mjit/code_block.rb deleted file mode 100644 index 21ae2386b7..0000000000 --- a/lib/ruby_vm/mjit/code_block.rb +++ /dev/null @@ -1,86 +0,0 @@ -module RubyVM::MJIT - class CodeBlock - # @param mem_block [Integer] JIT buffer address - # @param mem_size [Integer] JIT buffer size - # @param outliend [TrueClass,FalseClass] true for outlined CodeBlock - def initialize(mem_block:, mem_size:, outlined: false) - @comments = Hash.new { |h, k| h[k] = [] } - @mem_block = mem_block - @mem_size = mem_size - @write_pos = 0 - @outlined = outlined - end - - # @param asm [RubyVM::MJIT::Assembler] - def write(asm) - return 0 if @write_pos + asm.size >= @mem_size - - start_addr = write_addr - - # Write machine code - C.mjit_mark_writable - @write_pos += asm.assemble(start_addr) - C.mjit_mark_executable - - end_addr = write_addr - - # Convert comment indexes to addresses - asm.comments.each do |index, comments| - @comments[start_addr + index] += comments - end - asm.comments.clear - - # Dump disasm if --mjit-dump-disasm - if C.mjit_opts.dump_disasm && start_addr < end_addr - dump_disasm(start_addr, end_addr) - end - start_addr - end - - def set_write_addr(addr) - @write_pos = addr - @mem_block - @comments.delete(addr) # TODO: clean up old comments for all the overwritten range? - end - - def with_write_addr(addr) - old_write_pos = @write_pos - set_write_addr(addr) - yield - ensure - @write_pos = old_write_pos - end - - def write_addr - @mem_block + @write_pos - end - - def include?(addr) - (@mem_block...(@mem_block + @mem_size)).include?(addr) - end - - private - - def dump_disasm(from, to) - C.dump_disasm(from, to).each do |address, mnemonic, op_str| - @comments.fetch(address, []).each do |comment| - puts colorize(" # #{comment}", bold: true) - end - puts colorize(" 0x#{format("%x", address)}: #{mnemonic} #{op_str}") - end - puts - end - - def colorize(text, bold: false) - buf = +'' - buf << "\e[1m" if bold - buf << "\e[34m" if @outlined - buf << text - buf << "\e[0m" - buf - end - - def bold(text) - "\e[1m#{text}\e[0m" - end - end -end diff --git a/lib/ruby_vm/mjit/compiler.rb b/lib/ruby_vm/mjit/compiler.rb deleted file mode 100644 index 26e3d75d34..0000000000 --- a/lib/ruby_vm/mjit/compiler.rb +++ /dev/null @@ -1,188 +0,0 @@ -require 'ruby_vm/mjit/assembler' -require 'ruby_vm/mjit/block' -require 'ruby_vm/mjit/branch_stub' -require 'ruby_vm/mjit/code_block' -require 'ruby_vm/mjit/context' -require 'ruby_vm/mjit/exit_compiler' -require 'ruby_vm/mjit/insn_compiler' -require 'ruby_vm/mjit/instruction' -require 'ruby_vm/mjit/invariants' -require 'ruby_vm/mjit/jit_state' - -module RubyVM::MJIT - # Compilation status - KeepCompiling = :KeepCompiling - CantCompile = :CantCompile - EndBlock = :EndBlock - - # Ruby constants - Qtrue = Fiddle::Qtrue - Qfalse = Fiddle::Qfalse - Qnil = Fiddle::Qnil - Qundef = Fiddle::Qundef - - # Callee-saved registers - # TODO: support using r12/r13 here - EC = :r14 - CFP = :r15 - SP = :rbx - - # Scratch registers: rax, rcx - - class Compiler - attr_accessor :write_pos - - def self.decode_insn(encoded) - INSNS.fetch(C.rb_vm_insn_decode(encoded)) - end - - # @param mem_block [Integer] JIT buffer address - # @param mem_size [Integer] JIT buffer size - def initialize(mem_block, mem_size) - @cb = CodeBlock.new(mem_block: mem_block, mem_size: mem_size / 2) - @ocb = CodeBlock.new(mem_block: mem_block + mem_size / 2, mem_size: mem_size / 2, outlined: true) - @exit_compiler = ExitCompiler.new - @insn_compiler = InsnCompiler.new(@cb, @ocb, @exit_compiler) - - @leave_exit = Assembler.new.then do |asm| - @exit_compiler.compile_leave_exit(asm) - @ocb.write(asm) - end - end - - # 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 - - jit = JITState.new(iseq:, cfp:) - asm = Assembler.new - asm.comment("Block: #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{iseq.body.location.first_lineno}") - compile_prologue(asm) - compile_block(asm, jit:) - iseq.body.jit_func = @cb.write(asm) - rescue Exception => e - $stderr.puts e.full_message # TODO: check verbose - end - - # Compile a branch stub. - # @param branch_stub [RubyVM::MJIT::BranchStub] - # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` - # @param target0_p [TrueClass,FalseClass] - # @return [Integer] The starting address of the compiled branch stub - def branch_stub_hit(branch_stub, cfp, target0_p) - # Update cfp->pc for `jit.at_current_insn?` - target = target0_p ? branch_stub.target0 : branch_stub.target1 - cfp.pc = target.pc - - # Prepare the jump target - new_asm = Assembler.new.tap do |asm| - jit = JITState.new(iseq: branch_stub.iseq, cfp:) - compile_block(asm, jit:, pc: target.pc, ctx: target.ctx.dup) - end - - # Rewrite the branch stub - if @cb.write_addr == branch_stub.end_addr - # If the branch stub's jump is the last code, allow overwriting part of - # the old branch code with the new block code. - @cb.set_write_addr(branch_stub.start_addr) - branch_stub.shape = target0_p ? Next0 : Next1 - Assembler.new.tap do |branch_asm| - branch_stub.compile.call(branch_asm) - @cb.write(branch_asm) - end - - # Compile a fallthrough right after the new branch code - if target0_p - branch_stub.target0.address = @cb.write(new_asm) - else - branch_stub.target1.address = @cb.write(new_asm) - end - else - # Otherwise, just prepare the new block somewhere - if target0_p - branch_stub.target0.address = @cb.write(new_asm) - else - branch_stub.target1.address = @cb.write(new_asm) - end - - # Update jump destinations - @cb.with_write_addr(branch_stub.start_addr) do - branch_asm = Assembler.new - branch_stub.compile.call(branch_asm) - @cb.write(branch_asm) - end - end - - if target0_p - branch_stub.target0.address - else - branch_stub.target1.address - end - end - - private - - # 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 point') - - # Save callee-saved registers used by JITed code - asm.push(CFP) - asm.push(EC) - asm.push(SP) - - # Move arguments EC and CFP to dedicated registers - asm.mov(EC, :rdi) - asm.mov(CFP, :rsi) - - # Load sp to a dedicated register - asm.mov(SP, [CFP, C.rb_control_frame_t.offsetof(:sp)]) # rbx = cfp->sp - - # Setup cfp->jit_return - asm.mov(:rax, @leave_exit) - asm.mov([CFP, C.rb_control_frame_t.offsetof(:jit_return)], :rax) - end - - # @param asm [RubyVM::MJIT::Assembler] - def compile_block(asm, jit:, pc: jit.iseq.body.iseq_encoded.to_i, ctx: Context.new) - # Mark the block start address and prepare an exit code storage - jit.block = Block.new(pc:) - asm.block(jit.block) - - # Compile each insn - iseq = jit.iseq - index = (pc - iseq.body.iseq_encoded.to_i) / C.VALUE.size - 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 - - case status = @insn_compiler.compile(jit, ctx, asm, insn) - when KeepCompiling - index += insn.len - when EndBlock - # TODO: pad nops if entry exit exists - break - when CantCompile - @exit_compiler.compile_side_exit(jit, ctx, asm) - break - else - raise "compiling #{insn.name} returned unexpected status: #{status.inspect}" - end - end - - incr_counter(:compiled_block_count) - end - - def incr_counter(name) - if C.mjit_opts.stats - C.rb_mjit_counters[name][0] += 1 - end - end - end -end diff --git a/lib/ruby_vm/mjit/context.rb b/lib/ruby_vm/mjit/context.rb deleted file mode 100644 index 66d5f3b39b..0000000000 --- a/lib/ruby_vm/mjit/context.rb +++ /dev/null @@ -1,21 +0,0 @@ -module RubyVM::MJIT - class Context < Struct.new( - :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(stack_size: 0, sp_offset: 0) = super - - def stack_push(size = 1) - opnd = [SP, C.VALUE.size * self.sp_offset] - self.stack_size += size - self.sp_offset += size - opnd - end - - def stack_pop(size = 1) - self.stack_size -= size - self.sp_offset -= size - [SP, C.VALUE.size * self.sp_offset] - end - end -end diff --git a/lib/ruby_vm/mjit/exit_compiler.rb b/lib/ruby_vm/mjit/exit_compiler.rb deleted file mode 100644 index 0019eda7bd..0000000000 --- a/lib/ruby_vm/mjit/exit_compiler.rb +++ /dev/null @@ -1,119 +0,0 @@ -module RubyVM::MJIT - class ExitCompiler - def initialize - # TODO: Use GC offsets - @gc_refs = [] - end - - # Used for invalidating a block on entry. - # @param pc [Integer] - # @param asm [RubyVM::MJIT::Assembler] - def compile_entry_exit(pc, asm, cause:) - # Increment per-insn exit counter - incr_insn_exit(pc, asm) - - # TODO: Saving pc and sp may be needed later - - # Restore callee-saved registers - asm.comment("#{cause}: entry exit") - asm.pop(SP) - asm.pop(EC) - asm.pop(CFP) - - asm.mov(:rax, Qundef) - asm.ret - end - - # @param ocb [CodeBlock] - def compile_leave_exit(asm) - asm.comment('default cfp->jit_return') - - # Restore callee-saved registers - asm.pop(SP) - asm.pop(EC) - asm.pop(CFP) - - # :rax is written by #leave - asm.ret - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def compile_side_exit(jit, ctx, asm) - # Increment per-insn exit counter - incr_insn_exit(jit.pc, asm) - - # Fix pc/sp offsets for the interpreter - save_pc_and_sp(jit, ctx.dup, asm) # dup to avoid sp_offset update - - # Restore callee-saved registers - asm.comment("exit to interpreter on #{pc_to_insn(jit.pc).name}") - asm.pop(SP) - asm.pop(EC) - asm.pop(CFP) - - asm.mov(:rax, Qundef) - asm.ret - end - - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - # @param branch_stub [RubyVM::MJIT::BranchStub] - # @param target0_p [TrueClass,FalseClass] - def compile_branch_stub(ctx, asm, branch_stub, target0_p) - # Call rb_mjit_branch_stub_hit - asm.comment("branch stub hit: #{branch_stub.iseq.body.location.label}@#{C.rb_iseq_path(branch_stub.iseq)}:#{iseq_lineno(branch_stub.iseq, target0_p ? branch_stub.target0.pc : branch_stub.target1.pc)}") - asm.mov(:rdi, to_value(branch_stub)) - asm.mov(:esi, ctx.sp_offset) - asm.mov(:edx, target0_p ? 1 : 0) - asm.call(C.rb_mjit_branch_stub_hit) - - # Jump to the address returned by rb_mjit_stub_hit - asm.jmp(:rax) - end - - private - - def pc_to_insn(pc) - Compiler.decode_insn(C.VALUE.new(pc).*) - end - - # @param pc [Integer] - # @param asm [RubyVM::MJIT::Assembler] - def incr_insn_exit(pc, asm) - if C.mjit_opts.stats - insn = Compiler.decode_insn(C.VALUE.new(pc).*) - asm.comment("increment insn exit: #{insn.name}") - asm.mov(:rax, (C.mjit_insn_exits + insn.bin).to_i) - asm.add([:rax], 1) # TODO: lock - end - end - - # @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 (TODO: manage PC offset?) - asm.comment("save PC#{' and SP' if ctx.sp_offset != 0} to CFP") - 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 - - def iseq_lineno(iseq, pc) - C.rb_iseq_line_no(iseq, (pc - iseq.body.iseq_encoded.to_i) / C.VALUE.size) - end - end -end diff --git a/lib/ruby_vm/mjit/hooks.rb b/lib/ruby_vm/mjit/hooks.rb deleted file mode 100644 index 6aef14b56a..0000000000 --- a/lib/ruby_vm/mjit/hooks.rb +++ /dev/null @@ -1,32 +0,0 @@ -module RubyVM::MJIT::Hooks # :nodoc: all - C = RubyVM::MJIT.const_get(:C, false) - - def self.on_bop_redefined(_redefined_flag, _bop) - # C.mjit_cancel_all("BOP is redefined") - end - - def self.on_cme_invalidate(_cme) - # to be used later - end - - def self.on_ractor_spawn - # C.mjit_cancel_all("Ractor is spawned") - end - - def self.on_constant_state_changed(_id) - # to be used later - end - - def self.on_constant_ic_update(_iseq, _ic, _insn_idx) - # to be used later - end - - def self.on_tracing_invalidate_all(new_iseq_events) - # # Stop calling all JIT-ed code. We can't rewrite existing JIT-ed code to trace_ insns for now. - # # :class events are triggered only in ISEQ_TYPE_CLASS, but mjit_target_iseq_p ignores such iseqs. - # # Thus we don't need to cancel JIT-ed code for :class events. - # if new_iseq_events != C.RUBY_EVENT_CLASS - # C.mjit_cancel_all("TracePoint is enabled") - # end - end -end diff --git a/lib/ruby_vm/mjit/insn_compiler.rb b/lib/ruby_vm/mjit/insn_compiler.rb deleted file mode 100644 index 0ea0020f19..0000000000 --- a/lib/ruby_vm/mjit/insn_compiler.rb +++ /dev/null @@ -1,857 +0,0 @@ -module RubyVM::MJIT - class InsnCompiler - # @param ocb [CodeBlock] - # @param exit_compiler [RubyVM::MJIT::ExitCompiler] - def initialize(cb, ocb, exit_compiler) - @ocb = ocb - @exit_compiler = exit_compiler - @invariants = Invariants.new(cb, ocb, exit_compiler) - # freeze # workaround a binding.irb issue. TODO: resurrect this - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - # @param insn `RubyVM::MJIT::Instruction` - def compile(jit, ctx, asm, insn) - asm.incr_counter(:mjit_insns_count) - asm.comment("Insn: #{insn.name}") - - # 11/101 - case insn.name - # nop - # getlocal - # setlocal - # getblockparam - # setblockparam - # getblockparamproxy - # getspecial - # setspecial - # getinstancevariable - # setinstancevariable - # getclassvariable - # setclassvariable - # opt_getconstant_path - # getconstant - # setconstant - # getglobal - # setglobal - when :putnil then putnil(jit, ctx, asm) - when :putself then putself(jit, ctx, asm) - when :putobject then putobject(jit, ctx, asm) - # putspecialobject - # putstring - # concatstrings - # anytostring - # toregexp - # intern - # newarray - # newarraykwsplat - # duparray - # duphash - # expandarray - # concatarray - # splatarray - # newhash - # newrange - # pop - # dup - # dupn - # swap - # opt_reverse - # topn - # setn - # adjuststack - # defined - # checkmatch - # checkkeyword - # checktype - # defineclass - # definemethod - # definesmethod - # send - when :opt_send_without_block then opt_send_without_block(jit, ctx, asm) - # objtostring - # opt_str_freeze - # opt_nil_p - # opt_str_uminus - # opt_newarray_max - # opt_newarray_min - # invokesuper - # invokeblock - when :leave then leave(jit, ctx, asm) - # throw - # jump - # branchif - when :branchunless then branchunless(jit, ctx, asm) - # branchnil - # once - # opt_case_dispatch - when :opt_plus then opt_plus(jit, ctx, asm) - when :opt_minus then opt_minus(jit, ctx, asm) - # opt_mult - # opt_div - # opt_mod - # opt_eq - # opt_neq - when :opt_lt then opt_lt(jit, ctx, asm) - # opt_le - # opt_gt - # opt_ge - # opt_ltlt - # opt_and - # opt_or - # opt_aref - # opt_aset - # opt_aset_with - # opt_aref_with - # opt_length - # opt_size - # opt_empty_p - # opt_succ - # opt_not - # opt_regexpmatch2 - # invokebuiltin - # opt_invokebuiltin_delegate - # opt_invokebuiltin_delegate_leave - when :getlocal_WC_0 then getlocal_WC_0(jit, ctx, asm) - # setlocal_WC_0 - # setlocal_WC_1 - when :putobject_INT2FIX_0_ then putobject_INT2FIX_0_(jit, ctx, asm) - when :putobject_INT2FIX_1_ then putobject_INT2FIX_1_(jit, ctx, asm) - else CantCompile - end - end - - private - - # - # Insns - # - - # nop - # getlocal - # setlocal - # getblockparam - # setblockparam - # getblockparamproxy - # getspecial - # setspecial - # getinstancevariable - # setinstancevariable - # getclassvariable - # setclassvariable - # opt_getconstant_path - # getconstant - # setconstant - # getglobal - # setglobal - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def putnil(jit, ctx, asm) - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - asm.mov([SP, C.VALUE.size * ctx.stack_size], Qnil) - ctx.stack_push(1) - KeepCompiling - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def putself(jit, ctx, asm) - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:self)]) - asm.mov([SP, C.VALUE.size * ctx.stack_size], :rax) - ctx.stack_push(1) - KeepCompiling - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def putobject(jit, ctx, asm, val: jit.operand(0)) - # Push it to the stack - # TODO: GC offsets - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - if asm.imm32?(val) - asm.mov([SP, C.VALUE.size * ctx.stack_size], val) - else # 64-bit immediates can't be directly written to memory - asm.mov(:rax, val) - asm.mov([SP, C.VALUE.size * ctx.stack_size], :rax) - end - - ctx.stack_push(1) - KeepCompiling - end - - # putspecialobject - # putstring - # concatstrings - # anytostring - # toregexp - # intern - # newarray - # newarraykwsplat - # duparray - # duphash - # expandarray - # concatarray - # splatarray - # newhash - # newrange - # pop - # dup - # dupn - # swap - # opt_reverse - # topn - # setn - # adjuststack - # defined - # checkmatch - # checkkeyword - # checktype - # defineclass - # definemethod - # definesmethod - # send - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - # @param cd `RubyVM::MJIT::CPointer::Struct_rb_call_data` - def opt_send_without_block(jit, ctx, asm) - cd = C.rb_call_data.new(jit.operand(0)) - jit_call_method(jit, ctx, asm, cd) - end - - # objtostring - # opt_str_freeze - # opt_nil_p - # opt_str_uminus - # opt_newarray_max - # opt_newarray_min - # invokesuper - # invokeblock - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def leave(jit, ctx, asm) - assert_equal(ctx.stack_size, 1) - - compile_check_ints(jit, ctx, asm) - - asm.comment('pop stack frame') - asm.lea(:rax, [CFP, C.rb_control_frame_t.size]) - asm.mov(CFP, :rax) - asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], :rax) - - # Return a value (for compile_leave_exit) - ret_opnd = ctx.stack_pop - asm.mov(:rax, ret_opnd) - - # Set caller's SP and push a value to its stack (for JIT) - asm.mov(SP, [CFP, C.rb_control_frame_t.offsetof(:sp)]) # Note: SP is in the position after popping a receiver and arguments - asm.mov([SP], :rax) - - # Jump to cfp->jit_return - asm.jmp([CFP, -C.rb_control_frame_t.size + C.rb_control_frame_t.offsetof(:jit_return)]) - - EndBlock - end - - # throw - # jump - # branchif - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def branchunless(jit, ctx, asm) - # TODO: check ints for backward branches - # TODO: skip check for known truthy - - # This `test` sets ZF only for Qnil and Qfalse, which let jz jump. - asm.test([SP, C.VALUE.size * (ctx.stack_size - 1)], ~Qnil) - ctx.stack_pop(1) - - # Set stubs - branch_stub = BranchStub.new( - iseq: jit.iseq, - shape: Default, - target0: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * (jit.insn.len + jit.operand(0))), # branch target - target1: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * jit.insn.len), # fallthrough - ) - branch_stub.target0.address = Assembler.new.then do |ocb_asm| - @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, true) - @ocb.write(ocb_asm) - end - branch_stub.target1.address = Assembler.new.then do |ocb_asm| - @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, false) - @ocb.write(ocb_asm) - end - - # Jump to target0 on jz - branch_stub.compile = proc do |branch_asm| - branch_asm.comment("branchunless #{branch_stub.shape}") - branch_asm.stub(branch_stub) do - case branch_stub.shape - in Default - branch_asm.jz(branch_stub.target0.address) - branch_asm.jmp(branch_stub.target1.address) - in Next0 - branch_asm.jnz(branch_stub.target1.address) - in Next1 - branch_asm.jz(branch_stub.target0.address) - end - end - end - branch_stub.compile.call(asm) - - EndBlock - end - - # branchnil - # once - # opt_case_dispatch - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def opt_plus(jit, ctx, asm) - unless jit.at_current_insn? - defer_compilation(jit, ctx, asm) - return EndBlock - end - - comptime_recv = jit.peek_at_stack(1) - comptime_obj = jit.peek_at_stack(0) - - if fixnum?(comptime_recv) && fixnum?(comptime_obj) - # Generate a side exit before popping operands - side_exit = side_exit(jit, ctx) - - unless @invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_PLUS) - return CantCompile - end - - obj_opnd = ctx.stack_pop - recv_opnd = ctx.stack_pop - - asm.comment('guard recv is fixnum') # TODO: skip this with type information - asm.test(recv_opnd, C.RUBY_FIXNUM_FLAG) - asm.jz(side_exit) - - asm.comment('guard obj is fixnum') # TODO: skip this with type information - asm.test(obj_opnd, C.RUBY_FIXNUM_FLAG) - asm.jz(side_exit) - - asm.mov(:rax, recv_opnd) - asm.sub(:rax, 1) # untag - asm.mov(:rcx, obj_opnd) - asm.add(:rax, :rcx) - asm.jo(side_exit) - - dst_opnd = ctx.stack_push - asm.mov(dst_opnd, :rax) - - KeepCompiling - else - CantCompile # TODO: delegate to send - end - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def opt_minus(jit, ctx, asm) - unless jit.at_current_insn? - defer_compilation(jit, ctx, asm) - return EndBlock - end - - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - comptime_recv = jit.peek_at_stack(1) - comptime_obj = jit.peek_at_stack(0) - - if fixnum?(comptime_recv) && fixnum?(comptime_obj) - unless @invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_MINUS) - return CantCompile - end - - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - recv_index = ctx.stack_size - 2 - obj_index = ctx.stack_size - 1 - - asm.comment('guard recv is fixnum') # TODO: skip this with type information - asm.test([SP, C.VALUE.size * recv_index], C.RUBY_FIXNUM_FLAG) - asm.jz(side_exit(jit, ctx)) - - asm.comment('guard obj is fixnum') # TODO: skip this with type information - asm.test([SP, C.VALUE.size * obj_index], C.RUBY_FIXNUM_FLAG) - asm.jz(side_exit(jit, ctx)) - - asm.mov(:rax, [SP, C.VALUE.size * recv_index]) - asm.mov(:rcx, [SP, C.VALUE.size * obj_index]) - asm.sub(:rax, :rcx) - asm.jo(side_exit(jit, ctx)) - asm.add(:rax, 1) # re-tag - asm.mov([SP, C.VALUE.size * recv_index], :rax) - - ctx.stack_pop(1) - KeepCompiling - else - CantCompile # TODO: delegate to send - end - end - - # opt_mult - # opt_div - # opt_mod - # opt_eq - # opt_neq - - # @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 - - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - comptime_recv = jit.peek_at_stack(1) - comptime_obj = jit.peek_at_stack(0) - - if fixnum?(comptime_recv) && fixnum?(comptime_obj) - unless @invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_LT) - return CantCompile - end - - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - recv_index = ctx.stack_size - 2 - obj_index = ctx.stack_size - 1 - - asm.comment('guard recv is fixnum') # TODO: skip this with type information - asm.test([SP, C.VALUE.size * recv_index], C.RUBY_FIXNUM_FLAG) - asm.jz(side_exit(jit, ctx)) - - asm.comment('guard obj is fixnum') # TODO: skip this with type information - asm.test([SP, C.VALUE.size * obj_index], C.RUBY_FIXNUM_FLAG) - asm.jz(side_exit(jit, ctx)) - - asm.mov(:rax, [SP, C.VALUE.size * obj_index]) - asm.cmp([SP, C.VALUE.size * recv_index], :rax) - asm.mov(:rax, Qfalse) - asm.mov(:rcx, Qtrue) - asm.cmovl(:rax, :rcx) - asm.mov([SP, C.VALUE.size * recv_index], :rax) - - ctx.stack_pop(1) - KeepCompiling - else - CantCompile # TODO: delegate to send - end - end - - # opt_le - # opt_gt - # opt_ge - # opt_ltlt - # opt_and - # opt_or - # opt_aref - # opt_aset - # opt_aset_with - # opt_aref_with - # opt_length - # opt_size - # opt_empty_p - # opt_succ - # opt_not - # opt_regexpmatch2 - # invokebuiltin - # opt_invokebuiltin_delegate - # opt_invokebuiltin_delegate_leave - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def getlocal_WC_0(jit, ctx, asm) - # Get operands - idx = jit.operand(0) - level = 0 - - # Get EP - asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:ep)]) - - # Get a local variable - asm.mov(:rax, [:rax, -idx * C.VALUE.size]) - - # Push it to the stack - asm.mov([SP, C.VALUE.size * ctx.stack_size], :rax) - ctx.stack_push(1) - KeepCompiling - end - - # getlocal_WC_1 - # setlocal_WC_0 - # setlocal_WC_1 - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def putobject_INT2FIX_0_(jit, ctx, asm) - putobject(jit, ctx, asm, val: C.to_value(0)) - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def putobject_INT2FIX_1_(jit, ctx, asm) - putobject(jit, ctx, asm, val: C.to_value(1)) - end - - # - # Helpers - # - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def compile_check_ints(jit, ctx, asm) - asm.comment('RUBY_VM_CHECK_INTS(ec)') - asm.mov(:eax, [EC, C.rb_execution_context_t.offsetof(:interrupt_flag)]) - asm.test(:eax, :eax) - asm.jnz(side_exit(jit, ctx)) - end - - # vm_call_method (vm_sendish -> vm_call_general -> vm_call_method) - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - # @param cd `RubyVM::MJIT::CPointer::Struct_rb_call_data` - def jit_call_method(jit, ctx, asm, cd) - ci = cd.ci - argc = C.vm_ci_argc(ci) - mid = C.vm_ci_mid(ci) - flags = C.vm_ci_flag(ci) - - unless jit.at_current_insn? - defer_compilation(jit, ctx, asm) - return EndBlock - end - - if flags & C.VM_CALL_KW_SPLAT != 0 - # recv_index calculation may not work for this - asm.incr_counter(:send_kw_splat) - return CantCompile - end - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - recv_depth = argc + ((flags & C.VM_CALL_ARGS_BLOCKARG == 0) ? 0 : 1) - recv_index = ctx.stack_size - 1 - recv_depth - - comptime_recv = jit.peek_at_stack(recv_depth) - comptime_recv_klass = C.rb_class_of(comptime_recv) - - # Guard the receiver class (part of vm_search_method_fastpath) - if comptime_recv_klass.singleton_class? - asm.comment('guard known object with singleton class') - asm.mov(:rax, C.to_value(comptime_recv)) - asm.cmp([SP, C.VALUE.size * recv_index], :rax) - asm.jne(side_exit(jit, ctx)) - else - # TODO: support more classes - asm.incr_counter(:send_guard_known_object) - return CantCompile - end - - # Do method lookup (vm_cc_cme(cc) != NULL) - cme = C.rb_callable_method_entry(comptime_recv_klass, mid) - if cme.nil? - asm.incr_counter(:send_missing_cme) - return CantCompile # We don't support vm_call_method_name - end - - # The main check of vm_call_method before vm_call_method_each_type - case C.METHOD_ENTRY_VISI(cme) - when C.METHOD_VISI_PUBLIC - # You can always call public methods - when C.METHOD_VISI_PRIVATE - # Allow only callsites without a receiver - if flags & C.VM_CALL_FCALL == 0 - asm.incr_counter(:send_private) - return CantCompile - end - when C.METHOD_VISI_PROTECTED - asm.incr_counter(:send_protected) - return CantCompile # TODO: support this - else - # TODO: Change them to a constant and use case-in instead - raise 'unreachable' - end - - # Invalidate on redefinition (part of vm_search_method_fastpath) - @invariants.assume_method_lookup_stable(jit, cme) - - jit_call_method_each_type(jit, ctx, asm, ci, argc, flags, cme) - end - - # vm_call_method_each_type - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def jit_call_method_each_type(jit, ctx, asm, ci, argc, flags, cme) - case cme.def.type - when C.VM_METHOD_TYPE_ISEQ - jit_call_iseq_setup(jit, ctx, asm, ci, cme, flags, argc) - else - asm.incr_counter(:send_not_iseq) - return CantCompile - end - end - - # vm_call_iseq_setup - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def jit_call_iseq_setup(jit, ctx, asm, ci, cme, flags, argc) - iseq = def_iseq_ptr(cme.def) - opt_pc = jit_callee_setup_arg(jit, ctx, asm, ci, flags, iseq) - if opt_pc == CantCompile - # We hit some unsupported path of vm_callee_setup_arg - return CantCompile - end - - if flags & C.VM_CALL_TAILCALL != 0 - # We don't support vm_call_iseq_setup_tailcall - asm.incr_counter(:send_tailcall) - return CantCompile - end - jit_call_iseq_setup_normal(jit, ctx, asm, ci, cme, flags, argc, iseq) - end - - # vm_call_iseq_setup_normal (vm_call_iseq_setup_2 -> vm_call_iseq_setup_normal) - def jit_call_iseq_setup_normal(jit, ctx, asm, ci, cme, flags, argc, iseq) - # Save caller SP and PC before pushing a callee frame for backtrace and side exits - asm.comment('save SP to caller CFP') - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - sp_index = ctx.stack_size - 1 - argc - ((flags & C.VM_CALL_ARGS_BLOCKARG == 0) ? 0 : 1) # Pop receiver and arguments for side exits - asm.lea(:rax, [SP, C.VALUE.size * sp_index]) - asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], :rax) - - asm.comment('save PC to caller CFP') - next_pc = jit.pc + jit.insn.len * C.VALUE.size # Use the next one for backtrace and side exits - asm.mov(:rax, next_pc) - asm.mov([CFP, C.rb_control_frame_t.offsetof(:pc)], :rax) - - frame_type = C.VM_FRAME_MAGIC_METHOD | C.VM_ENV_FLAG_LOCAL - jit_push_frame(jit, ctx, asm, ci, cme, flags, argc, iseq, frame_type, next_pc) - end - - # vm_push_frame - # - # Frame structure: - # | args | locals | cme/cref | block_handler/prev EP | frame type (EP here) | stack bottom (SP here) - def jit_push_frame(jit, ctx, asm, ci, cme, flags, argc, iseq, frame_type, next_pc) - # TODO: stack overflow check - - local_size = iseq.body.local_table_size - iseq.body.param.size - local_size.times do |i| - asm.comment('set local variables') if i == 0 - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - local_index = ctx.stack_size + i - asm.mov([SP, C.VALUE.size * local_index], Qnil) - end - - assert_equal(ctx.sp_offset, ctx.stack_size) # TODO: support SP motion - sp_offset = ctx.stack_size + local_size + 3 - asm.add(SP, C.VALUE.size * sp_offset) - - asm.comment('set cme') - asm.mov(:rax, cme.to_i) - asm.mov([SP, C.VALUE.size * -3], :rax) - - asm.comment('set specval') - asm.mov([SP, C.VALUE.size * -2], C.VM_BLOCK_HANDLER_NONE) - - asm.comment('set frame type') - asm.mov([SP, C.VALUE.size * -1], frame_type) - - asm.comment('move CFP register to callee CFP') - asm.sub(CFP, C.rb_control_frame_t.size); - - # Not setting PC since JIT code will do that as needed - asm.comment('set SP to callee CFP') - asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], SP) - asm.comment('set ISEQ to callee CFP') - asm.mov(:rax, iseq.to_i) - asm.mov([CFP, C.rb_control_frame_t.offsetof(:iseq)], :rax) - asm.comment('set self to callee CFP') - self_index = -(1 + argc + ((flags & C.VM_CALL_ARGS_BLOCKARG == 0) ? 0 : 1) + local_size + 3) - asm.mov(:rax, [SP, C.VALUE.size * self_index]) - asm.mov([CFP, C.rb_control_frame_t.offsetof(:self)], :rax) - asm.comment('set EP to callee CFP') - asm.lea(:rax, [SP, C.VALUE.size * -1]) - asm.mov([CFP, C.rb_control_frame_t.offsetof(:ep)], :rax) - asm.comment('set block_code to callee CFP') - asm.mov([CFP, C.rb_control_frame_t.offsetof(:block_code)], 0) - asm.comment('set BP to callee CFP') - asm.mov([CFP, C.rb_control_frame_t.offsetof(:__bp__)], SP) # TODO: get rid of this!! - - # Stub cfp->jit_return - return_ctx = ctx.dup - return_ctx.stack_size -= argc + ((flags & C.VM_CALL_ARGS_BLOCKARG == 0) ? 0 : 1) # Pop args - return_ctx.sp_offset = 1 # SP is in the position after popping a receiver and arguments - branch_stub = BranchStub.new( - iseq: jit.iseq, - shape: Default, - target0: BranchTarget.new(ctx: return_ctx, pc: next_pc), - ) - branch_stub.target0.address = Assembler.new.then do |ocb_asm| - @exit_compiler.compile_branch_stub(return_ctx, ocb_asm, branch_stub, true) - @ocb.write(ocb_asm) - end - branch_stub.compile = proc do |branch_asm| - branch_asm.comment('set jit_return to callee CFP') - branch_asm.stub(branch_stub) do - case branch_stub.shape - in Default - branch_asm.mov(:rax, branch_stub.target0.address) - branch_asm.mov([CFP, C.rb_control_frame_t.offsetof(:jit_return)], :rax) - end - end - end - branch_stub.compile.call(asm) - - asm.comment('set callee CFP to ec->cfp') - asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) - - # Jump to a stub for the callee ISEQ - callee_ctx = Context.new - stub_next_block(iseq, iseq.body.iseq_encoded.to_i, callee_ctx, asm) - - EndBlock - end - - # vm_callee_setup_arg: Set up args and return opt_pc (or CantCompile) - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def jit_callee_setup_arg(jit, ctx, asm, ci, flags, iseq) - if flags & C.VM_CALL_KW_SPLAT == 0 - if C.rb_simple_iseq_p(iseq) - if jit_caller_setup_arg(jit, ctx, asm, flags) == CantCompile - return CantCompile - end - if jit_caller_remove_empty_kw_splat(jit, ctx, asm, flags) == CantCompile - return CantCompile - end - - if C.vm_ci_argc(ci) != iseq.body.param.lead_num - # argument_arity_error - return CantCompile - end - - return 0 - else - # We don't support the remaining `else if`s yet. - return CantCompile - end - end - - # We don't support setup_parameters_complex - return CantCompile - end - - # CALLER_SETUP_ARG: Return CantCompile if not supported - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def jit_caller_setup_arg(jit, ctx, asm, flags) - if flags & C.VM_CALL_ARGS_SPLAT != 0 - # We don't support vm_caller_setup_arg_splat - asm.incr_counter(:send_args_splat) - return CantCompile - end - if flags & (C.VM_CALL_KWARG | C.VM_CALL_KW_SPLAT) != 0 - # We don't support keyword args either - asm.incr_counter(:send_kwarg) - return CantCompile - end - end - - # CALLER_REMOVE_EMPTY_KW_SPLAT: Return CantCompile if not supported - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - # @param asm [RubyVM::MJIT::Assembler] - def jit_caller_remove_empty_kw_splat(jit, ctx, asm, flags) - if (flags & C.VM_CALL_KW_SPLAT) > 0 - # We don't support removing the last Hash argument - return CantCompile - end - end - - def assert_equal(left, right) - if left != right - raise "'#{left.inspect}' was not '#{right.inspect}'" - end - end - - def fixnum?(obj) - flag = C.RUBY_FIXNUM_FLAG - (C.to_value(obj) & flag) == flag - end - - # @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 - stub_next_block(jit.iseq, jit.pc, ctx, asm, comment: 'defer_compilation') - end - - def stub_next_block(iseq, pc, ctx, asm, comment: 'stub_next_block') - branch_stub = BranchStub.new( - iseq:, - shape: Default, - target0: BranchTarget.new(ctx:, pc:), - ) - branch_stub.target0.address = Assembler.new.then do |ocb_asm| - @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, true) - @ocb.write(ocb_asm) - end - branch_stub.compile = proc do |branch_asm| - branch_asm.comment(comment) - branch_asm.stub(branch_stub) do - case branch_stub.shape - in Default - branch_asm.jmp(branch_stub.target0.address) - in Next0 - # Just write the block without a jump - end - end - end - branch_stub.compile.call(asm) - end - - # @param jit [RubyVM::MJIT::JITState] - # @param ctx [RubyVM::MJIT::Context] - def side_exit(jit, ctx) - if side_exit = jit.side_exits[jit.pc] - return side_exit - end - asm = Assembler.new - @exit_compiler.compile_side_exit(jit, ctx, asm) - jit.side_exits[jit.pc] = @ocb.write(asm) - end - - def def_iseq_ptr(cme_def) - C.rb_iseq_check(cme_def.body.iseq.iseqptr) - end - end -end diff --git a/lib/ruby_vm/mjit/invariants.rb b/lib/ruby_vm/mjit/invariants.rb deleted file mode 100644 index 49cd2dee4d..0000000000 --- a/lib/ruby_vm/mjit/invariants.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'set' - -module RubyVM::MJIT - class Invariants - # @param cb [CodeBlock] - # @param ocb [CodeBlock] - # @param exit_compiler [RubyVM::MJIT::ExitCompiler] - def initialize(cb, ocb, exit_compiler) - @cb = cb - @ocb = ocb - @exit_compiler = exit_compiler - @bop_blocks = Set.new # TODO: actually invalidate this - @cme_blocks = Hash.new { |h, k| h[k] = Set.new } - - invariants = self - hooks = Module.new - hooks.define_method(:on_cme_invalidate) do |cme| - invariants.on_cme_invalidate(cme) - end - Hooks.singleton_class.prepend(hooks) - end - - # @param jit [RubyVM::MJIT::JITState] - # @param klass [Integer] - # @param op [Integer] - def assume_bop_not_redefined(jit, klass, op) - return false unless C.BASIC_OP_UNREDEFINED_P(klass, op) - - ensure_block_entry_exit(jit.block, cause: 'assume_bop_not_redefined') - @bop_blocks << jit.block - true - end - - # @param jit [RubyVM::MJIT::JITState] - def assume_method_lookup_stable(jit, cme) - ensure_block_entry_exit(jit.block, cause: 'assume_method_lookup_stable') - @cme_blocks[cme.to_i] << jit.block - end - - def on_cme_invalidate(cme) - @cme_blocks.fetch(cme.to_i, []).each do |block| - @cb.with_write_addr(block.start_addr) do - asm = Assembler.new - asm.jmp(block.entry_exit) - @cb.write(asm) - end - # TODO: re-generate branches that refer to this block - end - end - - private - - # @param block [RubyVM::MJIT::Block] - def ensure_block_entry_exit(block, cause:) - if block.entry_exit.nil? - asm = Assembler.new - @exit_compiler.compile_entry_exit(block.pc, asm, cause:) - block.entry_exit = @ocb.write(asm) - end - end - end -end diff --git a/lib/ruby_vm/mjit/jit_state.rb b/lib/ruby_vm/mjit/jit_state.rb deleted file mode 100644 index 1f888a02ae..0000000000 --- a/lib/ruby_vm/mjit/jit_state.rb +++ /dev/null @@ -1,30 +0,0 @@ -module RubyVM::MJIT - class JITState < Struct.new( - :iseq, # @param `RubyVM::MJIT::CPointer::Struct_rb_iseq_t` - :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) - :block, # @param [RubyVM::MJIT::Block] - :side_exits, # @param [Hash{ Integer => Integer }] { PC => address } - ) - def initialize(side_exits: {}, **) = super - - def insn - Compiler.decode_insn(C.VALUE.new(pc).*) - end - - def operand(index) - C.VALUE.new(pc)[index + 1] - end - - def at_current_insn? - pc == cfp.pc.to_i - end - - def peek_at_stack(depth_from_top) - raise 'not at current insn' unless at_current_insn? - offset = -(1 + depth_from_top) - value = (cfp.sp + offset).* - C.to_ruby(value) - end - end -end diff --git a/lib/ruby_vm/mjit/stats.rb b/lib/ruby_vm/mjit/stats.rb deleted file mode 100644 index bc5a30738e..0000000000 --- a/lib/ruby_vm/mjit/stats.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true -module RubyVM::MJIT - def self.runtime_stats - stats = {} - - # Insn exits - INSNS.each_value do |insn| - exits = C.mjit_insn_exits[insn.bin] - if exits > 0 - stats[:"exit_#{insn.name}"] = exits - end - end - - # Runtime stats - C.rb_mjit_runtime_counters.members.each do |member| - stats[member] = C.rb_mjit_counters.public_send(member) - end - - # Other stats are calculated here - stats[:side_exit_count] = stats.select { |name, _count| name.start_with?('exit_') }.sum(&:last) - if stats[:vm_insns_count] > 0 - retired_in_mjit = stats[:mjit_insns_count] - stats[:side_exit_count] - stats[:total_insns_count] = retired_in_mjit + stats[:vm_insns_count] - stats[:ratio_in_mjit] = 100.0 * retired_in_mjit / stats[:total_insns_count] - end - - stats - end - - class << self - private - - def print_stats - stats = runtime_stats - $stderr.puts("***MJIT: Printing MJIT statistics on exit***") - - print_counters(stats, prefix: 'send_', prompt: 'method call exit reasons') - - $stderr.puts "compiled_block_count: #{format('%10d', stats[:compiled_block_count])}" - $stderr.puts "side_exit_count: #{format('%10d', stats[:side_exit_count])}" - $stderr.puts "total_insns_count: #{format('%10d', stats[:total_insns_count])}" if stats.key?(:total_insns_count) - $stderr.puts "vm_insns_count: #{format('%10d', stats[:vm_insns_count])}" if stats.key?(:vm_insns_count) - $stderr.puts "mjit_insns_count: #{format('%10d', stats[:mjit_insns_count])}" - $stderr.puts "ratio_in_mjit: #{format('%9.1f', stats[:ratio_in_mjit])}%" if stats.key?(:ratio_in_mjit) - - print_exit_counts(stats) - end - - def print_counters(stats, prefix:, prompt:) - $stderr.puts("#{prompt}: ") - counters = stats.filter { |key, _| key.start_with?(prefix) } - counters.filter! { |_, value| value != 0 } - counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) } - - if counters.empty? - $stderr.puts(" (all relevant counters are zero)") - return - end - - counters = counters.to_a - counters.sort_by! { |(_, counter_value)| counter_value } - longest_name_length = counters.max_by { |(name, _)| name.length }.first.length - total = counters.sum { |(_, counter_value)| counter_value } - - counters.reverse_each do |(name, value)| - percentage = value.fdiv(total) * 100 - $stderr.printf(" %*s %10d (%4.1f%%)\n", longest_name_length, name, value, percentage) - end - end - - def print_exit_counts(stats, how_many: 20, padding: 2) - exits = stats.filter_map { |name, count| [name.to_s.delete_prefix('exit_'), count] if name.start_with?('exit_') }.to_h - return if exits.empty? - - top_exits = exits.sort_by { |_name, count| -count }.first(how_many).to_h - total_exits = exits.values.sum - $stderr.puts "Top-#{top_exits.size} most frequent exit ops (#{format("%.1f", 100.0 * top_exits.values.sum / total_exits)}% of exits):" - - name_width = top_exits.map { |name, _count| name.length }.max + padding - count_width = top_exits.map { |_name, count| count.to_s.length }.max + padding - top_exits.each do |name, count| - ratio = 100.0 * count / total_exits - $stderr.puts "#{format("%#{name_width}s", name)}: #{format("%#{count_width}d", count)} (#{format('%.1f', ratio)}%)" - end - end - end -end |
