Skip to content

Commit baec95c

Browse files
authored
ZJIT: Incorporate parameter loads into HIR (ruby#14659)
1 parent 05471d7 commit baec95c

3 files changed

Lines changed: 826 additions & 408 deletions

File tree

zjit/src/codegen.rs

Lines changed: 26 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::stats::{exit_counter_for_compile_error, incr_counter, incr_counter_by
1616
use crate::stats::{counter_ptr, with_time_stat, Counter, send_fallback_counter, Counter::{compile_time_ns, exit_compile_error}};
1717
use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr};
1818
use crate::backend::lir::{self, asm_comment, asm_ccall, Assembler, Opnd, Target, CFP, C_ARG_OPNDS, C_RET_OPND, EC, NATIVE_STACK_PTR, NATIVE_BASE_PTR, SCRATCH_OPND, SP};
19-
use crate::hir::{iseq_to_hir, Block, BlockId, BranchEdge, Invariant, MethodType, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType, SELF_PARAM_IDX};
19+
use crate::hir::{iseq_to_hir, BlockId, BranchEdge, Invariant, MethodType, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType};
2020
use crate::hir::{Const, FrameState, Function, Insn, InsnId};
2121
use crate::hir_type::{types, Type};
2222
use crate::options::get_option;
@@ -126,7 +126,7 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
126126
})?;
127127

128128
// Compile an entry point to the JIT code
129-
gen_entry(cb, iseq, &function, start_ptr).inspect_err(|err| {
129+
gen_entry(cb, iseq, start_ptr).inspect_err(|err| {
130130
debug!("{err:?}: gen_entry failed: {}", iseq_get_location(iseq, 0));
131131
})
132132
}
@@ -164,11 +164,10 @@ fn register_with_perf(iseq_name: String, start_ptr: usize, code_size: usize) {
164164
}
165165

166166
/// Compile a JIT entry
167-
fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function, function_ptr: CodePtr) -> Result<CodePtr, CompileError> {
167+
fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function_ptr: CodePtr) -> Result<CodePtr, CompileError> {
168168
// Set up registers for CFP, EC, SP, and basic block arguments
169169
let mut asm = Assembler::new();
170170
gen_entry_prologue(&mut asm, iseq);
171-
gen_entry_params(&mut asm, iseq, function.entry_block());
172171

173172
// Jump to the first block using a call instruction
174173
asm.ccall(function_ptr.raw_ptr(cb), vec![]);
@@ -409,8 +408,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
409408
Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id),
410409
Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))),
411410
Insn::GetGlobal { id, state } => gen_getglobal(jit, asm, *id, &function.frame_state(*state)),
412-
&Insn::GetLocal { ep_offset, level } => gen_getlocal_with_ep(asm, ep_offset, level),
413-
&Insn::SetLocal { val, ep_offset, level } => no_output!(gen_setlocal_with_ep(asm, opnd!(val), function.type_of(val), ep_offset, level)),
411+
&Insn::GetLocal { ep_offset, level, use_sp, .. } => gen_getlocal(asm, ep_offset, level, use_sp),
412+
&Insn::SetLocal { val, ep_offset, level } => no_output!(gen_setlocal(asm, opnd!(val), function.type_of(val), ep_offset, level)),
414413
Insn::GetConstantPath { ic, state } => gen_get_constant_path(jit, asm, *ic, &function.frame_state(*state)),
415414
Insn::SetIvar { self_val, id, val, state: _ } => no_output!(gen_setivar(asm, opnd!(self_val), *id, opnd!(val))),
416415
Insn::SideExit { state, reason } => no_output!(gen_side_exit(jit, asm, reason, &function.frame_state(*state))),
@@ -430,6 +429,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
430429
&Insn::ArrayExtend { left, right, state } => { no_output!(gen_array_extend(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state))) },
431430
&Insn::GuardShape { val, shape, state } => gen_guard_shape(jit, asm, opnd!(val), shape, &function.frame_state(state)),
432431
Insn::LoadPC => gen_load_pc(asm),
432+
Insn::LoadSelf => gen_load_self(),
433433
&Insn::LoadIvarEmbedded { self_val, id, index } => gen_load_ivar_embedded(asm, opnd!(self_val), id, index),
434434
&Insn::LoadIvarExtended { self_val, id, index } => gen_load_ivar_extended(asm, opnd!(self_val), id, index),
435435
&Insn::ArrayMax { state, .. }
@@ -537,19 +537,28 @@ fn gen_defined(jit: &JITState, asm: &mut Assembler, op_type: usize, obj: VALUE,
537537
/// Get a local variable from a higher scope or the heap. `local_ep_offset` is in number of VALUEs.
538538
/// We generate this instruction with level=0 only when the local variable is on the heap, so we
539539
/// can't optimize the level=0 case using the SP register.
540-
fn gen_getlocal_with_ep(asm: &mut Assembler, local_ep_offset: u32, level: u32) -> lir::Opnd {
540+
fn gen_getlocal(asm: &mut Assembler, local_ep_offset: u32, level: u32, use_sp: bool) -> lir::Opnd {
541+
let local_ep_offset = i32::try_from(local_ep_offset).unwrap_or_else(|_| panic!("Could not convert local_ep_offset {local_ep_offset} to i32"));
541542
if level > 0 {
542543
gen_incr_counter(asm, Counter::vm_read_from_parent_iseq_local_count);
543544
}
544-
let ep = gen_get_ep(asm, level);
545-
let offset = -(SIZEOF_VALUE_I32 * i32::try_from(local_ep_offset).unwrap_or_else(|_| panic!("Could not convert local_ep_offset {local_ep_offset} to i32")));
546-
asm.load(Opnd::mem(64, ep, offset))
545+
let local = if use_sp {
546+
assert_eq!(level, 0, "use_sp optimization should be used only for level=0 locals");
547+
let offset = -(SIZEOF_VALUE_I32 * (local_ep_offset + 1));
548+
Opnd::mem(64, SP, offset)
549+
} else {
550+
let ep = gen_get_ep(asm, level);
551+
let offset = -(SIZEOF_VALUE_I32 * local_ep_offset);
552+
Opnd::mem(64, ep, offset)
553+
};
554+
asm.load(local)
547555
}
548556

549557
/// Set a local variable from a higher scope or the heap. `local_ep_offset` is in number of VALUEs.
550558
/// We generate this instruction with level=0 only when the local variable is on the heap, so we
551559
/// can't optimize the level=0 case using the SP register.
552-
fn gen_setlocal_with_ep(asm: &mut Assembler, val: Opnd, val_type: Type, local_ep_offset: u32, level: u32) {
560+
fn gen_setlocal(asm: &mut Assembler, val: Opnd, val_type: Type, local_ep_offset: u32, level: u32) {
561+
let local_ep_offset = c_int::try_from(local_ep_offset).unwrap_or_else(|_| panic!("Could not convert local_ep_offset {local_ep_offset} to i32"));
553562
if level > 0 {
554563
gen_incr_counter(asm, Counter::vm_write_to_parent_iseq_local_count);
555564
}
@@ -558,12 +567,12 @@ fn gen_setlocal_with_ep(asm: &mut Assembler, val: Opnd, val_type: Type, local_ep
558567
// When we've proved that we're writing an immediate,
559568
// we can skip the write barrier.
560569
if val_type.is_immediate() {
561-
let offset = -(SIZEOF_VALUE_I32 * i32::try_from(local_ep_offset).unwrap_or_else(|_| panic!("Could not convert local_ep_offset {local_ep_offset} to i32")));
570+
let offset = -(SIZEOF_VALUE_I32 * local_ep_offset);
562571
asm.mov(Opnd::mem(64, ep, offset), val);
563572
} else {
564573
// We're potentially writing a reference to an IMEMO/env object,
565574
// so take care of the write barrier with a function.
566-
let local_index = c_int::try_from(local_ep_offset).ok().and_then(|idx| idx.checked_mul(-1)).unwrap_or_else(|| panic!("Could not turn {local_ep_offset} into a negative c_int"));
575+
let local_index = local_ep_offset * -1;
567576
asm_ccall!(asm, rb_vm_env_write, ep, local_index.into(), val);
568577
}
569578
}
@@ -834,6 +843,10 @@ fn gen_load_pc(asm: &mut Assembler) -> Opnd {
834843
asm.load(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC))
835844
}
836845

846+
fn gen_load_self() -> Opnd {
847+
Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF)
848+
}
849+
837850
fn gen_load_ivar_embedded(asm: &mut Assembler, self_val: Opnd, id: ID, index: u16) -> Opnd {
838851
// See ROBJECT_FIELDS() from include/ruby/internal/core/robject.h
839852

@@ -871,53 +884,6 @@ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) {
871884
asm.mov(SP, Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP));
872885
}
873886

874-
/// Assign method arguments to basic block arguments at JIT entry
875-
fn gen_entry_params(asm: &mut Assembler, iseq: IseqPtr, entry_block: &Block) {
876-
let num_params = entry_block.params().len() - 1; // -1 to exclude self
877-
if num_params > 0 {
878-
asm_comment!(asm, "set method params: {num_params}");
879-
880-
// Fill basic block parameters.
881-
// Doing it in reverse is load-bearing. High index params have memory slots that might
882-
// require using a register to fill. Filling them first avoids clobbering.
883-
for idx in (0..num_params).rev() {
884-
let param = param_opnd(idx + 1); // +1 for self
885-
let local = gen_entry_param(asm, iseq, idx);
886-
887-
// Funky offset adjustment to write into the native stack frame of the
888-
// HIR function we'll be calling into. This only makes sense in context
889-
// of the schedule of instructions in gen_entry() for the JIT entry point.
890-
//
891-
// The entry point needs to load VALUEs into native stack slots _before_ the
892-
// frame containing the slots exists. So, we anticipate the stack frame size
893-
// of the Function and subtract offsets based on that.
894-
//
895-
// native SP at entry point ─────►┌────────────┐ Native SP grows downwards
896-
// │ │ ↓ on all arches we support.
897-
// SP-0x8 ├────────────┤
898-
// │ │
899-
// where native SP SP-0x10├────────────┤
900-
// would be while │ │
901-
// the HIR function ────────────► └────────────┘
902-
// is running
903-
match param {
904-
Opnd::Mem(lir::Mem { base: _, disp, num_bits }) => {
905-
let param_slot = Opnd::mem(num_bits, NATIVE_STACK_PTR, disp - Assembler::frame_size());
906-
asm.mov(param_slot, local);
907-
}
908-
// Prepare for parallel move for locals in registers
909-
reg @ Opnd::Reg(_) => {
910-
asm.load_into(reg, local);
911-
}
912-
_ => unreachable!("on entry, params are either in memory or in reg. Got {param:?}")
913-
}
914-
915-
// Assign local variables to the basic block arguments
916-
}
917-
}
918-
asm.load_into(param_opnd(SELF_PARAM_IDX), Opnd::mem(VALUE_BITS, CFP, RUBY_OFFSET_CFP_SELF));
919-
}
920-
921887
/// Set branch params to basic block arguments
922888
fn gen_branch_params(jit: &mut JITState, asm: &mut Assembler, branch: &BranchEdge) {
923889
if branch.args.is_empty() {
@@ -941,28 +907,6 @@ fn gen_branch_params(jit: &mut JITState, asm: &mut Assembler, branch: &BranchEdg
941907
asm.parallel_mov(moves);
942908
}
943909

944-
/// Get a method parameter on JIT entry. As of entry, whether EP is escaped or not solely
945-
/// depends on the ISEQ type.
946-
fn gen_entry_param(asm: &mut Assembler, iseq: IseqPtr, local_idx: usize) -> lir::Opnd {
947-
let ep_offset = local_idx_to_ep_offset(iseq, local_idx);
948-
949-
// If the ISEQ does not escape EP, we can optimize the local variable access using the SP register.
950-
if !iseq_entry_escapes_ep(iseq) {
951-
// Create a reference to the local variable using the SP register. We assume EP == BP.
952-
// TODO: Implement the invalidation in rb_zjit_invalidate_no_ep_escape()
953-
let offs = -(SIZEOF_VALUE_I32 * (ep_offset + 1));
954-
Opnd::mem(64, SP, offs)
955-
} else {
956-
// Get the EP of the current CFP
957-
let ep_opnd = Opnd::mem(64, CFP, RUBY_OFFSET_CFP_EP);
958-
let ep_reg = asm.load(ep_opnd);
959-
960-
// Create a reference to the local variable using cfp->ep
961-
let offs = -(SIZEOF_VALUE_I32 * ep_offset);
962-
Opnd::mem(64, ep_reg, offs)
963-
}
964-
}
965-
966910
/// Compile a constant
967911
fn gen_const_value(val: VALUE) -> lir::Opnd {
968912
// Just propagate the constant value and generate nothing
@@ -1817,20 +1761,6 @@ fn build_side_exit(jit: &JITState, state: &FrameState, reason: SideExitReason, l
18171761
}
18181762
}
18191763

1820-
/// Return true if a given ISEQ is known to escape EP to the heap on entry.
1821-
///
1822-
/// As of vm_push_frame(), EP is always equal to BP. However, after pushing
1823-
/// a frame, some ISEQ setups call vm_bind_update_env(), which redirects EP.
1824-
fn iseq_entry_escapes_ep(iseq: IseqPtr) -> bool {
1825-
match unsafe { get_iseq_body_type(iseq) } {
1826-
// <main> frame is always associated to TOPLEVEL_BINDING.
1827-
ISEQ_TYPE_MAIN |
1828-
// Kernel#eval uses a heap EP when a Binding argument is not nil.
1829-
ISEQ_TYPE_EVAL => true,
1830-
_ => false,
1831-
}
1832-
}
1833-
18341764
/// Returne the maximum number of arguments for a block in a given function
18351765
fn max_num_params(function: &Function) -> usize {
18361766
let reverse_post_order = function.rpo();

zjit/src/cruby.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,10 @@ pub fn iseq_opcode_at_idx(iseq: IseqPtr, insn_idx: u32) -> u32 {
294294
unsafe { rb_iseq_opcode_at_pc(iseq, pc) as u32 }
295295
}
296296

297-
/// Return true if the ISEQ always uses a frame with escaped EP.
297+
/// Return true if a given ISEQ is known to escape EP to the heap on entry.
298+
///
299+
/// As of vm_push_frame(), EP is always equal to BP. However, after pushing
300+
/// a frame, some ISEQ setups call vm_bind_update_env(), which redirects EP.
298301
pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
299302
match unsafe { get_iseq_body_type(iseq) } {
300303
// The EP of the <main> frame points to TOPLEVEL_BINDING
@@ -305,6 +308,17 @@ pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
305308
}
306309
}
307310

311+
/// Index of the local variable that has a rest parameter if any
312+
pub fn iseq_rest_param_idx(iseq: IseqPtr) -> Option<i32> {
313+
if !iseq.is_null() && unsafe { get_iseq_flags_has_rest(iseq) } {
314+
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) };
315+
let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) };
316+
Some(opt_num + lead_num)
317+
} else {
318+
None
319+
}
320+
}
321+
308322
/// Iterate over all existing ISEQs
309323
pub fn for_each_iseq<F: FnMut(IseqPtr)>(mut callback: F) {
310324
unsafe extern "C" fn callback_wrapper(iseq: IseqPtr, data: *mut c_void) {

0 commit comments

Comments
 (0)