MJIT: Get rid of C.fprintf

Faster code generation and cleaner code.
This commit is contained in:
Takashi Kokubun 2022-11-27 22:23:35 -08:00
parent 87e7b640eb
commit 2c1c0d3df0
3 changed files with 47 additions and 52 deletions

View file

@ -19,46 +19,50 @@ module RubyVM::MJIT
] ]
class << Compiler = Module.new class << Compiler = Module.new
# @param iseq [RubyVM::MJIT::CPointer::Struct_rb_iseq_struct]
# @param funcname [String] # @param funcname [String]
def compile(f, iseq, funcname, id) # @param id [Integer]
# @return [String,NilClass]
def compile(iseq, funcname, id)
status = C.compile_status.new # not freed for now status = C.compile_status.new # not freed for now
status.compiled_iseq = iseq.body status.compiled_iseq = iseq.body
status.compiled_id = id status.compiled_id = id
init_compile_status(status, iseq.body, true) # not freed for now init_compile_status(status, iseq.body, true) # not freed for now
if iseq.body.ci_size > 0 && status.cc_entries_index == -1 if iseq.body.ci_size > 0 && status.cc_entries_index == -1
return false return nil
end end
init_ivar_compile_status(iseq.body, status) init_ivar_compile_status(iseq.body, status)
src = +''
if !status.compile_info.disable_send_cache && !status.compile_info.disable_inlining if !status.compile_info.disable_send_cache && !status.compile_info.disable_inlining
unless precompile_inlinable_iseqs(f, iseq, status) unless precompile_inlinable_iseqs(src, iseq, status)
return false return nil
end end
end end
C.fprintf(f, "VALUE\n#{funcname}(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp)\n{\n") src << "VALUE\n#{funcname}(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp)\n{\n"
success = compile_body(f, iseq, status) success = compile_body(src, iseq, status)
C.fprintf(f, "\n} // end of #{funcname}\n") src << "\n} // end of #{funcname}\n"
return success return success ? src : nil
rescue Exception => e # should we use rb_rescue in C instead? rescue Exception => e # should we use rb_rescue in C instead?
if C.mjit_opts.warnings || C.mjit_opts.verbose > 0 if C.mjit_opts.warnings || C.mjit_opts.verbose > 0
$stderr.puts "MJIT error: #{e.full_message}" $stderr.puts "MJIT error: #{e.full_message}"
end end
return false return nil
end end
private private
def compile_body(f, iseq, status) def compile_body(src, iseq, status)
status.success = true status.success = true
status.local_stack_p = !iseq.body.catch_except_p status.local_stack_p = !iseq.body.catch_except_p
if status.local_stack_p if status.local_stack_p
src = +" VALUE stack[#{iseq.body.stack_max}];\n" src << " VALUE stack[#{iseq.body.stack_max}];\n"
else else
src = +" VALUE *stack = reg_cfp->sp;\n" src << " VALUE *stack = reg_cfp->sp;\n"
end end
unless status.inlined_iseqs.nil? # i.e. compile root unless status.inlined_iseqs.nil? # i.e. compile root
@ -90,16 +94,15 @@ module RubyVM::MJIT
src << " }\n" src << " }\n"
end end
C.fprintf(f, src) compile_insns(0, 0, status, iseq.body, src)
compile_insns(0, 0, status, iseq.body, f) compile_cancel_handler(src, iseq.body, status)
compile_cancel_handler(f, iseq.body, status) src << "#undef GET_SELF\n"
C.fprintf(f, "#undef GET_SELF\n")
return status.success return status.success
end end
# Compile one conditional branch. If it has branchXXX insn, this should be # Compile one conditional branch. If it has branchXXX insn, this should be
# called multiple times for each branch. # called multiple times for each branch.
def compile_insns(stack_size, pos, status, body, f) def compile_insns(stack_size, pos, status, body, src)
branch = C.compile_branch.new # not freed for now branch = C.compile_branch.new # not freed for now
branch.stack_size = stack_size branch.stack_size = stack_size
branch.finish_p = false branch.finish_p = false
@ -108,8 +111,8 @@ module RubyVM::MJIT
insn = INSNS.fetch(C.rb_vm_insn_decode(body.iseq_encoded[pos])) insn = INSNS.fetch(C.rb_vm_insn_decode(body.iseq_encoded[pos]))
status.stack_size_for_pos[pos] = branch.stack_size status.stack_size_for_pos[pos] = branch.stack_size
C.fprintf(f, "\nlabel_#{pos}: /* #{insn.name} */\n") src << "\nlabel_#{pos}: /* #{insn.name} */\n"
pos = compile_insn(insn, pos, status, body.iseq_encoded + (pos+1), body, branch, f) pos = compile_insn(insn, pos, status, body.iseq_encoded + (pos+1), body, branch, src)
if status.success && branch.stack_size > body.stack_max if status.success && branch.stack_size > body.stack_max
if mjit_opts.warnings || mjit_opts.verbose > 0 if mjit_opts.warnings || mjit_opts.verbose > 0
$stderr.puts "MJIT warning: JIT stack size (#{branch.stack_size}) exceeded its max size (#{body.stack_max})" $stderr.puts "MJIT warning: JIT stack size (#{branch.stack_size}) exceeded its max size (#{body.stack_max})"
@ -126,11 +129,11 @@ module RubyVM::MJIT
# When you add a new instruction to insns.def, it would be nice to have JIT compilation support here but # When you add a new instruction to insns.def, it would be nice to have JIT compilation support here but
# it's optional. This JIT compiler just ignores ISeq which includes unknown instruction, and ISeq which # it's optional. This JIT compiler just ignores ISeq which includes unknown instruction, and ISeq which
# does not have it can be compiled as usual. # does not have it can be compiled as usual.
def compile_insn(insn, pos, status, operands, body, b, f) def compile_insn(insn, pos, status, operands, body, b, src)
sp_inc = C.mjit_call_attribute_sp_inc(insn.bin, operands) sp_inc = C.mjit_call_attribute_sp_inc(insn.bin, operands)
next_pos = pos + insn.len next_pos = pos + insn.len
result = compile_insn_entry(f, insn, b.stack_size, sp_inc, status.local_stack_p, pos, next_pos, insn.len, result = compile_insn_entry(insn, b.stack_size, sp_inc, status.local_stack_p, pos, next_pos, insn.len,
status.inlined_iseqs.nil?, status, operands, body) status.inlined_iseqs.nil?, status, operands, body)
if result.nil? if result.nil?
if C.mjit_opts.warnings || C.mjit_opts.verbose > 0 if C.mjit_opts.warnings || C.mjit_opts.verbose > 0
@ -138,9 +141,9 @@ module RubyVM::MJIT
end end
status.success = false status.success = false
else else
src, next_pos, finish_p, compile_insns_p = result result_src, next_pos, finish_p, compile_insns_p = result
C.fprintf(f, src) src << result_src
b.stack_size += sp_inc b.stack_size += sp_inc
if finish_p if finish_p
@ -148,9 +151,9 @@ module RubyVM::MJIT
end end
if compile_insns_p if compile_insns_p
if already_compiled?(status, pos + insn.len) if already_compiled?(status, pos + insn.len)
C.fprintf(f, "goto label_#{pos + insn.len};\n") src << "goto label_#{pos + insn.len};\n"
else else
compile_insns(b.stack_size, pos + insn.len, status, body, f) compile_insns(b.stack_size, pos + insn.len, status, body, src)
end end
end end
end end
@ -158,7 +161,7 @@ module RubyVM::MJIT
# If next_pos is already compiled and this branch is not finished yet, # If next_pos is already compiled and this branch is not finished yet,
# next instruction won't be compiled in C code next and will need `goto`. # next instruction won't be compiled in C code next and will need `goto`.
if !b.finish_p && next_pos < body.iseq_size && already_compiled?(status, next_pos) if !b.finish_p && next_pos < body.iseq_size && already_compiled?(status, next_pos)
C.fprintf(f, "goto label_#{next_pos};\n") src << "goto label_#{next_pos};\n"
# Verify stack size assumption is the same among multiple branches # Verify stack size assumption is the same among multiple branches
if status.stack_size_for_pos[next_pos] != b.stack_size if status.stack_size_for_pos[next_pos] != b.stack_size
@ -172,7 +175,7 @@ module RubyVM::MJIT
return next_pos return next_pos
end end
def compile_insn_entry(f, insn, stack_size, sp_inc, local_stack_p, pos, next_pos, insn_len, inlined_iseq_p, status, operands, body) def compile_insn_entry(insn, stack_size, sp_inc, local_stack_p, pos, next_pos, insn_len, inlined_iseq_p, status, operands, body)
finish_p = false finish_p = false
compile_insns = false compile_insns = false
@ -630,8 +633,8 @@ module RubyVM::MJIT
end end
# Print the block to cancel inlined method call. It's supporting only `opt_send_without_block` for now. # Print the block to cancel inlined method call. It's supporting only `opt_send_without_block` for now.
def compile_inlined_cancel_handler(f, body, inline_context) def compile_inlined_cancel_handler(src, body, inline_context)
src = +"\ncancel:\n" src << "\ncancel:\n"
src << " RB_DEBUG_COUNTER_INC(mjit_cancel);\n" src << " RB_DEBUG_COUNTER_INC(mjit_cancel);\n"
src << " rb_mjit_recompile_inlining(original_iseq);\n" src << " rb_mjit_recompile_inlining(original_iseq);\n"
@ -660,17 +663,16 @@ module RubyVM::MJIT
# We're not just returning Qundef here so that caller's normal cancel handler can # We're not just returning Qundef here so that caller's normal cancel handler can
# push back `stack` to `cfp->sp`. # push back `stack` to `cfp->sp`.
src << " return vm_exec(ec, false);\n" src << " return vm_exec(ec, false);\n"
C.fprintf(f, src)
end end
# Print the block to cancel JIT execution. # Print the block to cancel JIT execution.
def compile_cancel_handler(f, body, status) def compile_cancel_handler(src, body, status)
if status.inlined_iseqs.nil? # the current ISeq is being inlined if status.inlined_iseqs.nil? # the current ISeq is being inlined
compile_inlined_cancel_handler(f, body, status.inline_context) compile_inlined_cancel_handler(src, body, status.inline_context)
return return
end end
src = +"\nsend_cancel:\n" src << "\nsend_cancel:\n"
src << " RB_DEBUG_COUNTER_INC(mjit_cancel_send_inline);\n" src << " RB_DEBUG_COUNTER_INC(mjit_cancel_send_inline);\n"
src << " rb_mjit_recompile_send(original_iseq);\n" src << " rb_mjit_recompile_send(original_iseq);\n"
src << " goto cancel;\n" src << " goto cancel;\n"
@ -697,10 +699,9 @@ module RubyVM::MJIT
end end
end end
src << " return Qundef;\n" src << " return Qundef;\n"
C.fprintf(f, src)
end end
def precompile_inlinable_child_iseq(f, child_iseq, status, ci, cc, pos) def precompile_inlinable_child_iseq(src, child_iseq, status, ci, cc, pos)
child_status = C.compile_status.new # not freed for now child_status = C.compile_status.new # not freed for now
child_status.compiled_iseq = status.compiled_iseq child_status.compiled_iseq = status.compiled_iseq
child_status.compiled_id = status.compiled_id child_status.compiled_id = status.compiled_id
@ -714,20 +715,19 @@ module RubyVM::MJIT
end end
init_ivar_compile_status(child_iseq.body, child_status) init_ivar_compile_status(child_iseq.body, child_status)
src = +"ALWAYS_INLINE(static VALUE _mjit#{status.compiled_id}_inlined_#{pos}(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE orig_self, const rb_iseq_t *original_iseq));\n" src << "ALWAYS_INLINE(static VALUE _mjit#{status.compiled_id}_inlined_#{pos}(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE orig_self, const rb_iseq_t *original_iseq));\n"
src << "static inline VALUE\n_mjit#{status.compiled_id}_inlined_#{pos}(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE orig_self, const rb_iseq_t *original_iseq)\n{\n" src << "static inline VALUE\n_mjit#{status.compiled_id}_inlined_#{pos}(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE orig_self, const rb_iseq_t *original_iseq)\n{\n"
src << " const VALUE *orig_pc = reg_cfp->pc;\n" src << " const VALUE *orig_pc = reg_cfp->pc;\n"
src << " VALUE *orig_sp = reg_cfp->sp;\n" src << " VALUE *orig_sp = reg_cfp->sp;\n"
C.fprintf(f, src)
success = compile_body(f, child_iseq, child_status) success = compile_body(src, child_iseq, child_status)
C.fprintf(f, "\n} /* end of _mjit#{status.compiled_id}_inlined_#{pos} */\n\n") src << "\n} /* end of _mjit#{status.compiled_id}_inlined_#{pos} */\n\n"
return success; return success;
end end
def precompile_inlinable_iseqs(f, iseq, status) def precompile_inlinable_iseqs(src, iseq, status)
body = iseq.body body = iseq.body
pos = 0 pos = 0
while pos < body.iseq_size while pos < body.iseq_size
@ -745,7 +745,7 @@ module RubyVM::MJIT
$stderr.puts "JIT inline: #{child_location.label}@#{C.rb_iseq_path(child_iseq)}:#{C.rb_iseq_first_lineno(child_iseq)} " \ $stderr.puts "JIT inline: #{child_location.label}@#{C.rb_iseq_path(child_iseq)}:#{C.rb_iseq_first_lineno(child_iseq)} " \
"=> #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{C.rb_iseq_first_lineno(iseq)}" "=> #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{C.rb_iseq_first_lineno(iseq)}"
end end
if !precompile_inlinable_child_iseq(f, child_iseq, status, ci, cc, pos) if !precompile_inlinable_child_iseq(src, child_iseq, status, ci, cc, pos)
return false return false
end end
end end

View file

@ -114,14 +114,6 @@ module RubyVM::MJIT
iseq_addr = Primitive.cexpr! 'PTR2NUM((VALUE)rb_iseqw_to_iseq(iseqw))' iseq_addr = Primitive.cexpr! 'PTR2NUM((VALUE)rb_iseqw_to_iseq(iseqw))'
rb_iseq_t.new(iseq_addr) rb_iseq_t.new(iseq_addr)
end end
# TODO: remove this after migration
def fprintf(f, str)
Primitive.cstmt! %{
fprintf((FILE *)NUM2PTR(f), "%s", RSTRING_PTR(str));
return Qnil;
}
end
end end
### MJIT bindgen begin ### ### MJIT bindgen begin ###

View file

@ -123,11 +123,14 @@ mjit_compile(FILE *f, const rb_iseq_t *iseq, const char *funcname, int id)
mjit_call_p = false; // Avoid impacting JIT metrics by itself mjit_call_p = false; // Avoid impacting JIT metrics by itself
extern VALUE rb_mMJITCompiler; extern VALUE rb_mMJITCompiler;
bool success = RTEST(rb_funcall(rb_mMJITCompiler, rb_intern("compile"), 4, VALUE src = rb_funcall(rb_mMJITCompiler, rb_intern("compile"), 3,
PTR2NUM((VALUE)f), rb_ptr("rb_iseq_t", iseq), rb_str_new_cstr(funcname), INT2NUM(id))); rb_ptr("rb_iseq_t", iseq), rb_str_new_cstr(funcname), INT2NUM(id));
if (!NIL_P(src)) {
fprintf(f, "%s", RSTRING_PTR(src));
}
mjit_call_p = original_call_p; mjit_call_p = original_call_p;
return success; return !NIL_P(src);
} }
// //