Skip to content

Commit c7d56e9

Browse files
authored
ZJIT: Re-compile ISEQs invalidated by PatchPoint (ruby#15459)
1 parent 1c29fbe commit c7d56e9

File tree

8 files changed

+236
-93
lines changed

8 files changed

+236
-93
lines changed

zjit/src/backend/arm64/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -903,8 +903,8 @@ impl Assembler {
903903
}
904904
}
905905
}
906-
&mut Insn::PatchPoint { ref target, invariant, payload } => {
907-
split_patch_point(asm, target, invariant, payload);
906+
&mut Insn::PatchPoint { ref target, invariant, version } => {
907+
split_patch_point(asm, target, invariant, version);
908908
}
909909
_ => {
910910
asm.push_insn(insn);

zjit/src/backend/lir.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_
99
use crate::hir::{Invariant, SideExitReason};
1010
use crate::options::{TraceExits, debug, get_option};
1111
use crate::cruby::VALUE;
12-
use crate::payload::IseqPayload;
12+
use crate::payload::IseqVersionRef;
1313
use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_opcode, side_exit_counter, CompileError};
1414
use crate::virtualmem::CodePtr;
1515
use crate::asm::{CodeBlock, Label};
@@ -531,7 +531,7 @@ pub enum Insn {
531531
Or { left: Opnd, right: Opnd, out: Opnd },
532532

533533
/// Patch point that will be rewritten to a jump to a side exit on invalidation.
534-
PatchPoint { target: Target, invariant: Invariant, payload: *mut IseqPayload },
534+
PatchPoint { target: Target, invariant: Invariant, version: IseqVersionRef },
535535

536536
/// Make sure the last PatchPoint has enough space to insert a jump.
537537
/// We insert this instruction at the end of each block so that the jump
@@ -2358,8 +2358,8 @@ impl Assembler {
23582358
out
23592359
}
23602360

2361-
pub fn patch_point(&mut self, target: Target, invariant: Invariant, payload: *mut IseqPayload) {
2362-
self.push_insn(Insn::PatchPoint { target, invariant, payload });
2361+
pub fn patch_point(&mut self, target: Target, invariant: Invariant, version: IseqVersionRef) {
2362+
self.push_insn(Insn::PatchPoint { target, invariant, version });
23632363
}
23642364

23652365
pub fn pad_patch_point(&mut self) {

zjit/src/backend/x86_64/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,8 +640,8 @@ impl Assembler {
640640
};
641641
asm.store(dest, src);
642642
}
643-
&mut Insn::PatchPoint { ref target, invariant, payload } => {
644-
split_patch_point(asm, target, invariant, payload);
643+
&mut Insn::PatchPoint { ref target, invariant, version } => {
644+
split_patch_point(asm, target, invariant, version);
645645
}
646646
_ => {
647647
asm.push_insn(insn);

zjit/src/codegen.rs

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::invariants::{
1313
track_single_ractor_assumption, track_stable_constant_names_assumption, track_no_singleton_class_assumption
1414
};
1515
use crate::gc::append_gc_offsets;
16-
use crate::payload::{get_or_create_iseq_payload, get_or_create_iseq_payload_ptr, IseqCodePtrs, IseqPayload, IseqStatus};
16+
use crate::payload::{get_or_create_iseq_payload, IseqCodePtrs, IseqVersion, IseqVersionRef, IseqStatus};
1717
use crate::state::ZJITState;
1818
use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_ptr_for_opcode, send_without_block_fallback_counter_for_method_type, send_without_block_fallback_counter_for_optimized_method_type};
1919
use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ns, exit_compile_error}};
@@ -25,6 +25,9 @@ use crate::hir_type::{types, Type};
2525
use crate::options::get_option;
2626
use crate::cast::IntoUsize;
2727

28+
/// At the moment, we support recompiling each ISEQ only once.
29+
pub const MAX_ISEQ_VERSIONS: usize = 2;
30+
2831
/// Sentinel program counter stored in C frames when runtime checks are enabled.
2932
const PC_POISON: Option<*const VALUE> = if cfg!(feature = "runtime_checks") {
3033
Some(usize::MAX as *const VALUE)
@@ -37,6 +40,9 @@ struct JITState {
3740
/// Instruction sequence for the method being compiled
3841
iseq: IseqPtr,
3942

43+
/// ISEQ version that is being compiled, which will be used by PatchPoint
44+
version: IseqVersionRef,
45+
4046
/// Low-level IR Operands indexed by High-level IR's Instruction ID
4147
opnds: Vec<Option<Opnd>>,
4248

@@ -52,9 +58,10 @@ struct JITState {
5258

5359
impl JITState {
5460
/// Create a new JITState instance
55-
fn new(iseq: IseqPtr, num_insns: usize, num_blocks: usize) -> Self {
61+
fn new(iseq: IseqPtr, version: IseqVersionRef, num_insns: usize, num_blocks: usize) -> Self {
5662
JITState {
5763
iseq,
64+
version,
5865
opnds: vec![None; num_insns],
5966
labels: vec![None; num_blocks],
6067
jit_entries: Vec::default(),
@@ -134,11 +141,10 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
134141
}
135142

136143
/// Stub a branch for a JIT-to-JIT call
137-
fn gen_iseq_call(cb: &mut CodeBlock, caller_iseq: IseqPtr, iseq_call: &IseqCallRef) -> Result<(), CompileError> {
144+
pub fn gen_iseq_call(cb: &mut CodeBlock, iseq_call: &IseqCallRef) -> Result<(), CompileError> {
138145
// Compile a function stub
139146
let stub_ptr = gen_function_stub(cb, iseq_call.clone()).inspect_err(|err| {
140-
debug!("{err:?}: gen_function_stub failed: {} -> {}",
141-
iseq_get_location(caller_iseq, 0), iseq_get_location(iseq_call.iseq.get(), 0));
147+
debug!("{err:?}: gen_function_stub failed: {}", iseq_get_location(iseq_call.iseq.get(), 0));
142148
})?;
143149

144150
// Update the JIT-to-JIT call to call the stub
@@ -197,29 +203,36 @@ pub fn gen_entry_trampoline(cb: &mut CodeBlock) -> Result<CodePtr, CompileError>
197203
fn gen_iseq(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>) -> Result<IseqCodePtrs, CompileError> {
198204
// Return an existing pointer if it's already compiled
199205
let payload = get_or_create_iseq_payload(iseq);
200-
match &payload.status {
201-
IseqStatus::Compiled(code_ptrs) => return Ok(code_ptrs.clone()),
202-
IseqStatus::CantCompile(err) => return Err(err.clone()),
203-
IseqStatus::NotCompiled => {},
206+
let last_status = payload.versions.last().map(|version| &unsafe { version.as_ref() }.status);
207+
match last_status {
208+
Some(IseqStatus::Compiled(code_ptrs)) => return Ok(code_ptrs.clone()),
209+
Some(IseqStatus::CantCompile(err)) => return Err(err.clone()),
210+
_ => {},
211+
}
212+
// If the ISEQ already hax MAX_ISEQ_VERSIONS, do not compile a new version.
213+
if payload.versions.len() == MAX_ISEQ_VERSIONS {
214+
return Err(CompileError::IseqVersionLimitReached);
204215
}
205216

206217
// Compile the ISEQ
207-
let code_ptrs = gen_iseq_body(cb, iseq, function, payload);
218+
let mut version = IseqVersion::new(iseq);
219+
let code_ptrs = gen_iseq_body(cb, iseq, version, function);
208220
match &code_ptrs {
209221
Ok(code_ptrs) => {
210-
payload.status = IseqStatus::Compiled(code_ptrs.clone());
222+
unsafe { version.as_mut() }.status = IseqStatus::Compiled(code_ptrs.clone());
211223
incr_counter!(compiled_iseq_count);
212224
}
213225
Err(err) => {
214-
payload.status = IseqStatus::CantCompile(err.clone());
226+
unsafe { version.as_mut() }.status = IseqStatus::CantCompile(err.clone());
215227
incr_counter!(failed_iseq_count);
216228
}
217229
}
230+
payload.versions.push(version);
218231
code_ptrs
219232
}
220233

221234
/// Compile an ISEQ into machine code
222-
fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>, payload: &mut IseqPayload) -> Result<IseqCodePtrs, CompileError> {
235+
fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, mut version: IseqVersionRef, function: Option<&Function>) -> Result<IseqCodePtrs, CompileError> {
223236
// If we ran out of code region, we shouldn't attempt to generate new code.
224237
if cb.has_dropped_bytes() {
225238
return Err(CompileError::OutOfMemory);
@@ -232,23 +245,23 @@ fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>,
232245
};
233246

234247
// Compile the High-level IR
235-
let (iseq_code_ptrs, gc_offsets, iseq_calls) = gen_function(cb, iseq, function)?;
248+
let (iseq_code_ptrs, gc_offsets, iseq_calls) = gen_function(cb, iseq, version, function)?;
236249

237250
// Stub callee ISEQs for JIT-to-JIT calls
238251
for iseq_call in iseq_calls.iter() {
239-
gen_iseq_call(cb, iseq, iseq_call)?;
252+
gen_iseq_call(cb, iseq_call)?;
240253
}
241254

242255
// Prepare for GC
243-
payload.iseq_calls.extend(iseq_calls);
244-
append_gc_offsets(iseq, &gc_offsets);
256+
unsafe { version.as_mut() }.outgoing.extend(iseq_calls);
257+
append_gc_offsets(iseq, version, &gc_offsets);
245258
Ok(iseq_code_ptrs)
246259
}
247260

248261
/// Compile a function
249-
fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Result<(IseqCodePtrs, Vec<CodePtr>, Vec<IseqCallRef>), CompileError> {
262+
fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, function: &Function) -> Result<(IseqCodePtrs, Vec<CodePtr>, Vec<IseqCallRef>), CompileError> {
250263
let num_spilled_params = max_num_params(function).saturating_sub(ALLOC_REGS.len());
251-
let mut jit = JITState::new(iseq, function.num_insns(), function.num_blocks());
264+
let mut jit = JITState::new(iseq, version, function.num_insns(), function.num_blocks());
252265
let mut asm = Assembler::new_with_stack_slots(num_spilled_params);
253266

254267
// Compile each basic block
@@ -724,17 +737,16 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, state: &FrameState, bf
724737

725738
/// Record a patch point that should be invalidated on a given invariant
726739
fn gen_patch_point(jit: &mut JITState, asm: &mut Assembler, invariant: &Invariant, state: &FrameState) {
727-
let payload_ptr = get_or_create_iseq_payload_ptr(jit.iseq);
728740
let invariant = *invariant;
729741
let exit = build_side_exit(jit, state);
730742

731743
// Let compile_exits compile a side exit. Let scratch_split lower it with split_patch_point.
732-
asm.patch_point(Target::SideExit { exit, reason: PatchPoint(invariant) }, invariant, payload_ptr);
744+
asm.patch_point(Target::SideExit { exit, reason: PatchPoint(invariant) }, invariant, jit.version);
733745
}
734746

735747
/// This is used by scratch_split to lower PatchPoint into PadPatchPoint and PosMarker.
736748
/// It's called at scratch_split so that we can use the Label after side-exit deduplication in compile_exits.
737-
pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invariant, payload_ptr: *mut IseqPayload) {
749+
pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invariant, version: IseqVersionRef) {
738750
let Target::Label(exit_label) = *target else {
739751
unreachable!("PatchPoint's target should have been lowered to Target::Label by compile_exits: {target:?}");
740752
};
@@ -747,25 +759,25 @@ pub fn split_patch_point(asm: &mut Assembler, target: &Target, invariant: Invari
747759
let side_exit_ptr = cb.resolve_label(exit_label);
748760
match invariant {
749761
Invariant::BOPRedefined { klass, bop } => {
750-
track_bop_assumption(klass, bop, code_ptr, side_exit_ptr, payload_ptr);
762+
track_bop_assumption(klass, bop, code_ptr, side_exit_ptr, version);
751763
}
752764
Invariant::MethodRedefined { klass: _, method: _, cme } => {
753-
track_cme_assumption(cme, code_ptr, side_exit_ptr, payload_ptr);
765+
track_cme_assumption(cme, code_ptr, side_exit_ptr, version);
754766
}
755767
Invariant::StableConstantNames { idlist } => {
756-
track_stable_constant_names_assumption(idlist, code_ptr, side_exit_ptr, payload_ptr);
768+
track_stable_constant_names_assumption(idlist, code_ptr, side_exit_ptr, version);
757769
}
758770
Invariant::NoTracePoint => {
759-
track_no_trace_point_assumption(code_ptr, side_exit_ptr, payload_ptr);
771+
track_no_trace_point_assumption(code_ptr, side_exit_ptr, version);
760772
}
761773
Invariant::NoEPEscape(iseq) => {
762-
track_no_ep_escape_assumption(iseq, code_ptr, side_exit_ptr, payload_ptr);
774+
track_no_ep_escape_assumption(iseq, code_ptr, side_exit_ptr, version);
763775
}
764776
Invariant::SingleRactorMode => {
765-
track_single_ractor_assumption(code_ptr, side_exit_ptr, payload_ptr);
777+
track_single_ractor_assumption(code_ptr, side_exit_ptr, version);
766778
}
767779
Invariant::NoSingletonClass { klass } => {
768-
track_no_singleton_class_assumption(klass, code_ptr, side_exit_ptr, payload_ptr);
780+
track_no_singleton_class_assumption(klass, code_ptr, side_exit_ptr, version);
769781
}
770782
}
771783
});
@@ -2383,8 +2395,9 @@ c_callable! {
23832395
// code path can be made read-only. But you still need the check as is while holding the VM lock in any case.
23842396
let cb = ZJITState::get_code_block();
23852397
let payload = get_or_create_iseq_payload(iseq);
2386-
let compile_error = match &payload.status {
2387-
IseqStatus::CantCompile(err) => Some(err),
2398+
let last_status = payload.versions.last().map(|version| &unsafe { version.as_ref() }.status);
2399+
let compile_error = match last_status {
2400+
Some(IseqStatus::CantCompile(err)) => Some(err),
23882401
_ if cb.has_dropped_bytes() => Some(&CompileError::OutOfMemory),
23892402
_ => None,
23902403
};
@@ -2398,7 +2411,15 @@ c_callable! {
23982411

23992412
// Otherwise, attempt to compile the ISEQ. We have to mark_all_executable() beyond this point.
24002413
let code_ptr = with_time_stat(compile_time_ns, || function_stub_hit_body(cb, &iseq_call));
2414+
if code_ptr.is_ok() {
2415+
if let Some(version) = payload.versions.last_mut() {
2416+
unsafe { version.as_mut() }.incoming.push(iseq_call);
2417+
}
2418+
}
24012419
let code_ptr = code_ptr.unwrap_or_else(|compile_error| {
2420+
// We'll use this Rc again, so increment the ref count decremented by from_raw.
2421+
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
2422+
24022423
prepare_for_exit(iseq, cfp, sp, &compile_error);
24032424
ZJITState::get_exit_trampoline_with_counter()
24042425
});
@@ -2715,3 +2736,37 @@ impl IseqCall {
27152736
});
27162737
}
27172738
}
2739+
2740+
#[cfg(test)]
2741+
mod tests {
2742+
use crate::codegen::MAX_ISEQ_VERSIONS;
2743+
use crate::cruby::test_utils::*;
2744+
use crate::payload::*;
2745+
2746+
#[test]
2747+
fn test_max_iseq_versions() {
2748+
eval(&format!("
2749+
TEST = -1
2750+
def test = TEST
2751+
2752+
# compile and invalidate MAX+1 times
2753+
i = 0
2754+
while i < {MAX_ISEQ_VERSIONS} + 1
2755+
test; test # compile a version
2756+
2757+
Object.send(:remove_const, :TEST)
2758+
TEST = i
2759+
2760+
i += 1
2761+
end
2762+
"));
2763+
2764+
// It should not exceed MAX_ISEQ_VERSIONS
2765+
let iseq = get_method_iseq("self", "test");
2766+
let payload = get_or_create_iseq_payload(iseq);
2767+
assert_eq!(payload.versions.len(), MAX_ISEQ_VERSIONS);
2768+
2769+
// The last call should not discard the JIT code
2770+
assert!(matches!(unsafe { payload.versions.last().unwrap().as_ref() }.status, IseqStatus::Compiled(_)));
2771+
}
2772+
}

0 commit comments

Comments
 (0)