Skip to content

Commit 8c53ca9

Browse files
committed
ZJIT: Locals in stackmap
1 parent 69b13f2 commit 8c53ca9

6 files changed

Lines changed: 194 additions & 72 deletions

File tree

vm.c

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,8 @@ vm_make_env_each(const rb_execution_context_t * const ec, rb_control_frame_t *co
11281128
if (VM_FRAME_RUBYFRAME_P(cfp)) {
11291129
rb_yjit_invalidate_ep_is_bp(iseq);
11301130
rb_zjit_invalidate_no_ep_escape(iseq);
1131+
// Also use the stack map to put values into the format expected below
1132+
rb_zjit_spill_frame(cfp);
11311133
}
11321134

11331135
/*
@@ -2883,6 +2885,35 @@ vm_exec_loop(rb_execution_context_t *ec, enum ruby_tag_type state,
28832885
}
28842886

28852887
#if USE_ZJIT
2888+
// Write a ZJIT frame's off-stack Ruby slots (operands and locals) into VM-stack
2889+
// memory using its stack map, decoding downward from cfp->sp. Does not touch
2890+
// any frame metadata (pc/_iseq/block_code/jit_return).
2891+
static void
2892+
zjit_write_frame_stack(rb_control_frame_t *cfp, const zjit_jit_frame_t *jit_frame)
2893+
{
2894+
int32_t stack_size = (int32_t)jit_frame->stack_size;
2895+
if (stack_size > 0) {
2896+
VALUE *stack = cfp->sp;
2897+
for (int32_t i = 0; i < stack_size; i++) {
2898+
VALUE entry = jit_frame->stack[i];
2899+
if (ZJIT_STACK_MAP_VREG_P(entry)) {
2900+
// Decode a native stack slot offset generated by ZJIT's backend.
2901+
// It's an offset from NATIVE_BASE_PTR, which is copied into
2902+
// cfp->jit_return, to the encoded stack slot.
2903+
stack--;
2904+
*stack = ((VALUE *)cfp->jit_return)[-(ssize_t)ZJIT_STACK_MAP_VREG_INDEX(entry)];
2905+
}
2906+
else if (ZJIT_STACK_MAP_SKIP_P(entry)) {
2907+
stack -= ZJIT_STACK_MAP_SKIP_SIZE(entry);
2908+
}
2909+
else {
2910+
stack--;
2911+
*stack = entry;
2912+
}
2913+
}
2914+
}
2915+
}
2916+
28862917
// Materialize JITFrame-enabled CFP into interpreter-compatible CFP
28872918
static void
28882919
zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp, bool materialize_target)
@@ -2916,27 +2947,7 @@ zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cf
29162947
// the exiting frame is already written by compile_exit_save_state()
29172948
// and skipped here after materialize_exit_trampoline clears its
29182949
// jit_return, so this restores older ZJIT frames from stack maps.
2919-
int32_t stack_size = (int32_t)jit_frame->stack_size;
2920-
if (stack_size > 0) {
2921-
VALUE *stack = cfp->sp;
2922-
for (int32_t i = 0; i < stack_size; i++) {
2923-
VALUE entry = jit_frame->stack[i];
2924-
if (ZJIT_STACK_MAP_VREG_P(entry)) {
2925-
// Decode a native stack slot offset generated by ZJIT's backend.
2926-
// It's an offset from NATIVE_BASE_PTR, which is copied into
2927-
// cfp->jit_return, to the encoded stack slot.
2928-
stack--;
2929-
*stack = ((VALUE *)cfp->jit_return)[-(ssize_t)ZJIT_STACK_MAP_VREG_INDEX(entry)];
2930-
}
2931-
else if (ZJIT_STACK_MAP_SKIP_P(entry)) {
2932-
stack -= ZJIT_STACK_MAP_SKIP_SIZE(entry);
2933-
}
2934-
else {
2935-
stack--;
2936-
*stack = entry;
2937-
}
2938-
}
2939-
}
2950+
zjit_write_frame_stack(cfp, jit_frame);
29402951
cfp->jit_return = 0;
29412952
}
29422953
if (end_cfp == cfp) break;
@@ -2957,6 +2968,19 @@ rb_zjit_materialize_frames_for_longjmp(const rb_execution_context_t *ec, rb_cont
29572968
// stack and survives longjmp. Materialize only the frames unwound above it.
29582969
zjit_materialize_frames(ec, cfp, !ec->tag->zjit_frame_active);
29592970
}
2971+
2972+
// Spill a single ZJIT frame's off-stack Ruby slots (operands and locals) into
2973+
// VM-stack memory, leaving the frame lazy (jit_return and pc/_iseq intact).
2974+
// vm_make_env_each() calls this so an escaping env can MEMCPY live locals out
2975+
// of VM memory.
2976+
void
2977+
rb_zjit_spill_frame(rb_control_frame_t *cfp)
2978+
{
2979+
if (!rb_zjit_enabled_p) return;
2980+
if (CFP_ZJIT_FRAME_P(cfp)) {
2981+
zjit_write_frame_stack(cfp, CFP_ZJIT_FRAME(cfp));
2982+
}
2983+
}
29602984
#endif
29612985

29622986
static inline VALUE

zjit.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ bool rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, sha
9898
bool rb_zjit_str_resurrect_fastpath(VALUE str, bool chilled, size_t *size_out, VALUE *flags_out, long *len_out, size_t *byte_size_out);
9999
bool rb_zjit_array_dup_can_fastpath(VALUE ary, size_t *alloc_size_out, VALUE *flags_out, long *len_out);
100100
void rb_zjit_range_new_fastpath(bool exclude_end, size_t *alloc_size_out, VALUE *flags_out);
101+
void rb_zjit_spill_frame(rb_control_frame_t *cfp);
101102

102103
// Special value for cfp->jit_return that means "this is a C method frame, use
103104
// rb_zjit_c_frame as the JITFrame". We don't control the native stack layout
@@ -140,6 +141,7 @@ static inline void rb_zjit_invalidate_root_box(void) {}
140141
static inline void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame) {}
141142
static inline void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp) {}
142143
static inline void rb_zjit_materialize_frames_for_longjmp(const rb_execution_context_t *ec, rb_control_frame_t *cfp) {}
144+
static inline void rb_zjit_spill_frame(rb_control_frame_t *cfp) {}
143145
static inline const zjit_jit_frame_t *CFP_ZJIT_FRAME(const rb_control_frame_t *cfp) { return NULL; }
144146
#endif // #if USE_ZJIT
145147

zjit/src/codegen.rs

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,9 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, function: &Function, s
962962
} else {
963963
// Anything can happen inside builtin functions
964964
gen_prepare_non_leaf_call(jit, asm, function, state);
965+
// cexpr!/cstmt! builtins read this frame's locals (its params) directly
966+
// from EP, so make them authoritative in memory.
967+
gen_spill_locals(jit, asm, state);
965968
}
966969

967970
let mut cargs = vec![EC];
@@ -1049,7 +1052,12 @@ fn gen_ccall_with_frame(
10491052
gen_write_jit_frame(asm, state, 0);
10501053
gen_save_sp(asm, caller_stack_size);
10511054
gen_spill_stack(jit, asm, function, state);
1052-
gen_spill_locals(jit, asm, state);
1055+
// A passed block can read or write this frame's locals through its EP, so
1056+
// make them authoritative in memory. Without a block, the stack map is
1057+
// enough to reconstruct locals lazily on exception/binding.
1058+
if block.is_some() {
1059+
gen_spill_locals(jit, asm, state);
1060+
}
10531061

10541062
let block_handler_specval = if let Some(BlockHandler::BlockIseq(block_iseq)) = block {
10551063
// Change cfp->block_code in the current frame. See vm_caller_setup_arg_block().
@@ -1144,7 +1152,10 @@ fn gen_ccall_variadic(
11441152
gen_write_jit_frame(asm, state, 0);
11451153
gen_save_sp(asm, caller_stack_size);
11461154
gen_spill_stack(jit, asm, function, state);
1147-
gen_spill_locals(jit, asm, state);
1155+
// A passed block can read or write this frame's locals through its EP.
1156+
if block.is_some() {
1157+
gen_spill_locals(jit, asm, state);
1158+
}
11481159

11491160
let block_handler_specval = if let Some(BlockHandler::BlockIseq(blockiseq)) = block {
11501161
gen_block_handler_specval(asm, blockiseq)
@@ -1505,6 +1516,10 @@ fn gen_send(
15051516
gen_trace_send_fallback(asm, &reason);
15061517

15071518
gen_prepare_fallback_call(jit, asm, function, state);
1519+
// A literal block passed here can read or write this frame's locals through its EP.
1520+
if !blockiseq.is_null() {
1521+
gen_spill_locals(jit, asm, state);
1522+
}
15081523
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
15091524
unsafe extern "C" {
15101525
fn rb_vm_send(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
@@ -1530,6 +1545,10 @@ fn gen_send_forward(
15301545
gen_trace_send_fallback(asm, &reason);
15311546

15321547
gen_prepare_fallback_call(jit, asm, function, state);
1548+
// A literal block passed here can read or write this frame's locals through its EP.
1549+
if !blockiseq.is_null() {
1550+
gen_spill_locals(jit, asm, state);
1551+
}
15331552

15341553
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
15351554
unsafe extern "C" {
@@ -1590,7 +1609,10 @@ fn gen_push_inline_frame(
15901609
gen_write_jit_frame(asm, state, 0);
15911610
gen_save_sp(asm, stack_size);
15921611

1593-
gen_spill_locals(jit, asm, state);
1612+
// A passed block can read or write this frame's locals through its EP.
1613+
if blockiseq.is_some() {
1614+
gen_spill_locals(jit, asm, state);
1615+
}
15941616

15951617
// This mirrors vm_caller_setup_arg_block() for the `blockiseq != NULL` case.
15961618
// The HIR specialization guards ensure we will only reach here for literal blocks,
@@ -1729,7 +1751,11 @@ fn gen_send_iseq_direct(
17291751
let jit_frame = gen_write_jit_frame(asm, state, stack_map.len());
17301752
gen_save_sp(asm, stack_size);
17311753

1732-
gen_spill_locals(jit, asm, state);
1754+
// A passed block can read or write this frame's locals through its EP.
1755+
// Without a block, the stack map reconstructs locals lazily on demand.
1756+
if block.is_some() {
1757+
gen_spill_locals(jit, asm, state);
1758+
}
17331759
asm.stack_map(stack_map, jit_frame, state.depth);
17341760

17351761
// This mirrors vm_caller_setup_arg_block() in for the `blockiseq != NULL` case.
@@ -2018,6 +2044,10 @@ fn gen_invokesuper(
20182044
gen_trace_send_fallback(asm, &reason);
20192045

20202046
gen_prepare_fallback_call(jit, asm, function, state);
2047+
// A literal block passed here can read or write this frame's locals through its EP.
2048+
if !blockiseq.is_null() {
2049+
gen_spill_locals(jit, asm, state);
2050+
}
20212051
asm_comment!(asm, "call super with dynamic dispatch");
20222052
unsafe extern "C" {
20232053
fn rb_vm_invokesuper(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
@@ -2043,6 +2073,10 @@ fn gen_invokesuperforward(
20432073
gen_trace_send_fallback(asm, &reason);
20442074

20452075
gen_prepare_fallback_call(jit, asm, function, state);
2076+
// A literal block passed here can read or write this frame's locals through its EP.
2077+
if !blockiseq.is_null() {
2078+
gen_spill_locals(jit, asm, state);
2079+
}
20462080
asm_comment!(asm, "call super with dynamic dispatch (forwarding)");
20472081
unsafe extern "C" {
20482082
fn rb_vm_invokesuperforward(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE;
@@ -3373,7 +3407,6 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, function: &Function, sta
33733407
fn gen_prepare_fallback_call(jit: &JITState, asm: &mut Assembler, function: &Function, state: &FrameState) {
33743408
gen_write_jit_frame(asm, state, 0);
33753409
gen_save_sp(asm, state.stack_size());
3376-
gen_spill_locals(jit, asm, state);
33773410
gen_spill_stack(jit, asm, function, state);
33783411
}
33793412

@@ -3384,31 +3417,46 @@ fn build_stack_map(jit: &JITState, function: &Function, state: &FrameState) -> V
33843417
let mut stack = Vec::new();
33853418
let mut current_state = state.clone();
33863419
loop {
3387-
stack.extend(current_state.stack().rev().copied().map(|insn_id| {
3420+
let to_entry = |insn_id| {
33883421
let opnd = jit.get_opnd(insn_id);
33893422
assert!(
33903423
matches!(opnd, Opnd::Value(_) | Opnd::VReg { .. }),
33913424
"FrameState should only reference Opnd::Value or Opnd::VReg, but got: {opnd:?}",
33923425
);
33933426
StackMapEntry::Opnd(opnd)
3394-
}));
3427+
};
3428+
3429+
// Operand stack, top-down.
3430+
stack.extend(current_state.stack().rev().copied().map(to_entry));
3431+
// Frame environment data (me/cref, specval, flags) already lives in memory.
3432+
stack.push(StackMapEntry::Skip(VM_ENV_DATA_SIZE.to_usize()));
3433+
// Locals, top-down (local[L-1] .. local[0]). They land at fixed
3434+
// EP-relative slots because we write down from cfp->sp.
3435+
//
3436+
// Locals backed by memory are accessed in mainline code,
3437+
// so we skip over those slots to not interfere and clobber.
3438+
// (The stack map is not the 1st write to the slot.)
3439+
let spilled_locals = current_state.spilled_locals();
3440+
for (idx, &insn_id) in current_state.locals().enumerate().rev() {
3441+
if spilled_locals.contains(&(idx as u32)) {
3442+
stack.push(StackMapEntry::Skip(1));
3443+
} else {
3444+
stack.push(to_entry(insn_id));
3445+
}
3446+
}
33953447

33963448
let Some(caller) = current_state.caller() else {
33973449
break;
33983450
};
3399-
stack.push(StackMapEntry::Skip(inline_frame_stack_gap(current_state.iseq)));
3451+
// Skip the callee's receiver slot below its local table. We currently
3452+
// never map out the stack for `invokeblock`, which doesn't put a
3453+
// receiver on cfp->sp stack.
3454+
stack.push(StackMapEntry::Skip(1));
34003455
current_state = function.frame_state(caller);
34013456
}
34023457
stack
34033458
}
34043459

3405-
fn inline_frame_stack_gap(iseq: IseqPtr) -> usize {
3406-
// The extra slot is for the callee's receiver below its local table.
3407-
// We currently never map out the stack for `invokeblock`, which doesn't
3408-
// put a receiver on cfp->sp stack.
3409-
1 + unsafe { get_iseq_body_local_table_size(iseq) }.to_usize() + VM_ENV_DATA_SIZE.to_usize()
3410-
}
3411-
34123460
/// Prepare for calling a C function that may call an arbitrary method.
34133461
/// Use gen_prepare_leaf_call_with_gc() if the method is leaf but allocates objects.
34143462
fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, function: &Function, state: &FrameState) {
@@ -3421,9 +3469,6 @@ fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, function: &Fun
34213469
// Remember the stack map in case it raises an exception
34223470
// and the interpreter uses the stack for handling the exception
34233471
asm.stack_map(stack_map, jit_frame, state.depth);
3424-
3425-
// Spill locals in case the method looks at caller Bindings
3426-
gen_spill_locals(jit, asm, state);
34273472
}
34283473

34293474
/// Frame metadata written by gen_push_frame()

zjit/src/codegen_tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6742,9 +6742,9 @@ fn test_polymorphic_getivar_complex_shape() {
67426742

67436743
/// When a method with keyword defaults contains a block that creates a lambda,
67446744
/// the lambda causes EP escape, which globally patches NoEPEscape PatchPoints.
6745-
/// On subsequent calls the PatchPoint side exit (which uses without_locals())
6746-
/// must not leave stale keyword default values in the frame. We solve this by
6747-
/// invalidating the ISEQ version on EP escape so the interpreter takes over.
6745+
/// On subsequent calls the PatchPoint side exit must not leave stale keyword
6746+
/// default values in the frame. We solve this by invalidating the ISEQ version
6747+
/// on EP escape so the interpreter takes over.
67486748
#[test]
67496749
fn test_ep_escape_preserves_keyword_default() {
67506750
set_call_threshold(1);

0 commit comments

Comments
 (0)