Skip to content

Commit 0916e47

Browse files
committed
ZJIT: Skip redundant cfp->sp stores
gen_save_sp() stored cfp->sp before every C call and JIT-to-JIT call, even when an earlier call in the same basic block had already written the same value and nothing could have changed it in between: callees write only their own cfp->sp, side exits and exception unwinding never return to the same code, and rb_zjit_materialize_frames() only reads cfp->sp. Track the stack size of the last cfp->sp store emitted in the current basic block in the Assembler and skip stores that would write the same value again. The cache is cleared at block boundaries (but preserved across split_block_jump() fall-throughs, which have a single predecessor), when PushInlineFrame/PopInlineFrame change which frame the SP/CFP registers point at, and after gen_prepare_fallback_call(), whose VM helpers execute an instruction on the current frame and move cfp->sp (e.g. vm_sendish() popping the receiver and arguments), which the test_regression_cfp_sp_set_correctly_before_leaf_gc_call test caught.
1 parent 058aa08 commit 0916e47

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

zjit/src/backend/lir.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,13 @@ pub struct Assembler {
20202020
/// allocator spill slot each one is saved to right after FrameSetup. Every exit path
20212021
/// restores them from these slots. Filled in by [`Self::plan_callee_saved_saves`].
20222022
callee_saved_saves: Vec<(Reg, usize)>,
2023+
2024+
/// The stack size whose corresponding value is known to be already stored
2025+
/// in the current frame's cfp->sp by an earlier store in the current basic
2026+
/// block. Used by codegen to skip redundant cfp->sp stores. Cleared at
2027+
/// block boundaries and whenever codegen changes which frame the SP/CFP
2028+
/// registers point at.
2029+
saved_sp_stack_size: Option<usize>,
20232030
}
20242031

20252032
impl Assembler
@@ -2038,6 +2045,7 @@ impl Assembler
20382045
stack_map: None,
20392046
allow_callee_saved: false,
20402047
callee_saved_saves: Vec::default(),
2048+
saved_sp_stack_size: None,
20412049
}
20422050
}
20432051

@@ -2120,6 +2128,37 @@ impl Assembler
21202128

21212129
pub fn set_current_block(&mut self, block_id: BlockId) {
21222130
self.current_block_id = block_id;
2131+
// Conservatively assume the new block can be entered with a different
2132+
// frame memory state, e.g. join blocks with multiple predecessors.
2133+
self.clear_frame_write_cache();
2134+
}
2135+
2136+
/// Like set_current_block(), but preserves the frame write cache. Only
2137+
/// safe when the new block is a fresh fall-through block whose single
2138+
/// predecessor is the current block, like the ones split_block_jump()
2139+
/// creates: control falls through linearly, so frame writes recorded so
2140+
/// far still describe the memory state.
2141+
pub fn set_current_block_fall_through(&mut self, block_id: BlockId) {
2142+
let saved_sp_stack_size = self.saved_sp_stack_size;
2143+
self.set_current_block(block_id);
2144+
self.saved_sp_stack_size = saved_sp_stack_size;
2145+
}
2146+
2147+
/// Record that the current frame's cfp->sp is being stored for `stack_size`.
2148+
/// Returns true if an earlier store in the current block already wrote the
2149+
/// same value, in which case the store can be skipped.
2150+
pub fn note_sp_save(&mut self, stack_size: usize) -> bool {
2151+
if self.saved_sp_stack_size == Some(stack_size) {
2152+
return true;
2153+
}
2154+
self.saved_sp_stack_size = Some(stack_size);
2155+
false
2156+
}
2157+
2158+
/// Forget all frame writes recorded by note_sp_save().
2159+
/// Call this when the SP/CFP registers change which frame they point at.
2160+
pub fn clear_frame_write_cache(&mut self) {
2161+
self.saved_sp_stack_size = None;
21232162
}
21242163

21252164
pub fn current_block(&mut self) -> &mut BasicBlock {

zjit/src/codegen.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ impl Assembler {
141141
emit(self, target);
142142
self.jmp(Target::Block(Box::new(fall_through_edge)));
143143

144-
self.set_current_block(fall_through_target);
144+
self.set_current_block_fall_through(fall_through_target);
145145

146146
let label = jit.get_label(self, fall_through_target, hir_block_id);
147147
self.write_label(label);
@@ -3013,6 +3013,10 @@ fn gen_push_inline_frame(
30133013
let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
30143014
asm.mov(CFP, new_cfp);
30153015
asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
3016+
3017+
// The SP/CFP registers now point at the callee frame, so frame writes
3018+
// recorded for the caller frame no longer describe the same locations.
3019+
asm.clear_frame_write_cache();
30163020
}
30173021

30183022
/// Pop the interpreter frame for an inlined callee, restoring the caller's SP and CFP.
@@ -3034,6 +3038,11 @@ fn gen_pop_inline_frame(
30343038
asm_comment!(asm, "restore caller CFP after inline");
30353039
asm.add_into(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
30363040
asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP as i32), CFP);
3041+
3042+
// The SP/CFP registers now point at the caller frame again, so frame
3043+
// writes recorded for the callee frame no longer describe the same
3044+
// locations.
3045+
asm.clear_frame_write_cache();
30373046
}
30383047

30393048
/// Compile a direct call to an ISEQ method.
@@ -5366,6 +5375,14 @@ fn gen_prepare_leaf_call_with_gc(asm: &mut Assembler, state: &FrameState) {
53665375

53675376
/// Save the current SP on the CFP
53685377
fn gen_save_sp(asm: &mut Assembler, stack_size: usize) {
5378+
// Skip the store if an earlier store in this block already wrote the same
5379+
// value. Nothing modifies a frame's cfp->sp while it's not the innermost
5380+
// frame: callees write only their own cfp->sp, side exits and exception
5381+
// unwinding never return to this code, and rb_zjit_materialize_frames()
5382+
// only reads cfp->sp.
5383+
if asm.note_sp_save(stack_size) {
5384+
return;
5385+
}
53695386
// Update cfp->sp which will be read by the interpreter. We also have the SP register in JIT
53705387
// code, and ZJIT's codegen currently assumes the SP register doesn't move, e.g. gen_param().
53715388
// So we don't update the SP register here. We could update the SP register to avoid using
@@ -5420,6 +5437,12 @@ fn gen_prepare_fallback_call(jit: &JITState, asm: &mut Assembler, function: &Fun
54205437
gen_save_sp(asm, state.stack_size());
54215438
gen_spill_locals(jit, asm, state);
54225439
gen_spill_stack(jit, asm, function, state);
5440+
5441+
// VM fallback helpers execute an instruction on the current frame, which
5442+
// moves cfp->sp (e.g. vm_sendish() pops the receiver and arguments), so
5443+
// frame writes recorded above no longer describe the frame's memory once
5444+
// the helper returns. Forget them so that later calls store fresh values.
5445+
asm.clear_frame_write_cache();
54235446
}
54245447

54255448
/// Build entries for Ruby stack values that need materialization. The actual

0 commit comments

Comments
 (0)