Skip to content

Commit cd6badb

Browse files
committed
wip
1 parent b0754fb commit cd6badb

6 files changed

Lines changed: 68 additions & 15 deletions

File tree

test/ruby/test_yjit.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,6 +1861,27 @@ def test_yjit_prelude_kernel_prepend
18611861
end
18621862
end
18631863

1864+
def test_exceptional_entry_into_env_escaped_before_yjit_enablement
1865+
threshold = 2
1866+
assert_separately(["--disable-all", "--yjit-disable", "--yjit-call-threshold=#{threshold}"], <<~RUBY)
1867+
def run
1868+
@captured_env = ->{}
1869+
RubyVM::YJIT.enable
1870+
1871+
i = 0
1872+
while i < #{threshold}
1873+
next_i = i + 1
1874+
from_break = tap { break i + 1 } # break from the block generates an exceptional entry
1875+
assert_equal(from_break, next_i)
1876+
i = next_i
1877+
end
1878+
end
1879+
1880+
run
1881+
assert_equal(#{threshold}, @captured_env.binding.local_variable_get(:i))
1882+
RUBY
1883+
end
1884+
18641885
private
18651886

18661887
def code_gc_helpers

yjit/src/codegen.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -234,20 +234,36 @@ impl<'a> JITState<'a> {
234234
result
235235
}
236236

237-
/// Return true if the current ISEQ could escape an environment.
237+
/// Return true if the JIT code can use [`Self::assume_no_ep_escape`]
238+
/// and will run with an on-stack (`!VM_ENV_ESCAPED_P`) environment.
238239
///
239-
/// As of vm_push_frame(), EP is always equal to BP. However, after pushing
240-
/// a frame, some ISEQ setups call vm_bind_update_env(), which redirects EP.
241-
/// Also, some method calls escape the environment to the heap.
242-
fn escapes_ep(&self) -> bool {
240+
/// ## Reasoning about ISEQs that are not currently running
241+
///
242+
/// As of vm_push_frame() and its JIT code equivalent, EP is always equal to BP (the
243+
/// environment is on-stack and has not escaped). We can usually assume this is the starting
244+
/// condition upon entry into JIT code. However, after pushing a frame and before entry into
245+
/// JIT code, some ISEQ setups call vm_bind_update_env(), which redirects EP.
246+
///
247+
/// ## After making the assumption
248+
///
249+
/// After JIT code entry, many ruby operations can have the environment escape to heap. These
250+
/// are handled by [`crate::invariants`].
251+
///
252+
/// Exceptional entry through jit_exec_exception() is an extreme case of the environment state
253+
/// changing between vm_push_frame() and entry into JIT code. We reject exceptional entries
254+
/// with an escaped environment.
255+
fn can_assume_on_stack_env(&self) -> bool {
243256
match unsafe { get_iseq_body_type(self.iseq) } {
244257
// <main> frame is always associated to TOPLEVEL_BINDING.
245258
ISEQ_TYPE_MAIN |
246259
// Kernel#eval uses a heap EP when a Binding argument is not nil.
247-
ISEQ_TYPE_EVAL => true,
248-
// If this ISEQ has previously escaped EP, give up the optimization.
249-
_ if iseq_escapes_ep(self.iseq) => true,
250-
_ => false,
260+
ISEQ_TYPE_EVAL => false,
261+
// When compiling for an already escaped environment, it's not on-stack.
262+
_ if unsafe { self.iseq == get_cfp_iseq(self.get_cfp()) && cfp_env_has_escaped(self.get_cfp()) } => false,
263+
// If we've seen this ISEQ run with an escaped environment, give up the optimization
264+
// to avoid excessive invalidations (even though it may be fine for soundness).
265+
_ if seen_escaped_env(self.iseq) => false,
266+
_ => true,
251267
}
252268
}
253269

@@ -376,8 +392,8 @@ impl<'a> JITState<'a> {
376392
if jit_ensure_block_entry_exit(self, asm).is_none() {
377393
return false; // out of space, give up
378394
}
379-
if self.escapes_ep() {
380-
return false; // EP has been escaped in this ISEQ. disable the optimization to avoid an invalidation loop.
395+
if !self.can_assume_on_stack_env() {
396+
return false; // Unsound or unprofitable to make the assumption
381397
}
382398
self.no_ep_escape = true;
383399
true
@@ -2509,7 +2525,7 @@ fn gen_getlocal_generic(
25092525
level: u32,
25102526
) -> Option<CodegenStatus> {
25112527
// Split the block if we need to invalidate this instruction when EP escapes
2512-
if level == 0 && !jit.escapes_ep() && !jit.at_compile_target() {
2528+
if level == 0 && jit.can_assume_on_stack_env() && !jit.at_compile_target() {
25132529
return jit.defer_compilation(asm);
25142530
}
25152531

@@ -2610,7 +2626,7 @@ fn gen_setlocal_generic(
26102626
}
26112627

26122628
// Split the block if we need to invalidate this instruction when EP escapes
2613-
if level == 0 && !jit.escapes_ep() && !jit.at_compile_target() {
2629+
if level == 0 && jit.can_assume_on_stack_env() && !jit.at_compile_target() {
26142630
return jit.defer_compilation(asm);
26152631
}
26162632

yjit/src/cruby.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,13 @@ impl From<VALUE> for u16 {
598598
}
599599
}
600600

601+
/// Check whether a control frame has an escaped environment
602+
pub unsafe fn cfp_env_has_escaped(cfp: *mut rb_control_frame_struct) -> bool {
603+
use crate::utils::IntoUsize;
604+
let ep = get_cfp_ep(cfp);
605+
0 != ep.offset(VM_ENV_DATA_INDEX_FLAGS as isize).read().0 & VM_ENV_FLAG_ESCAPED.as_usize()
606+
}
607+
601608
/// Produce a Ruby string from a Rust string slice
602609
pub fn rust_str_to_ruby(str: &str) -> VALUE {
603610
unsafe { rb_utf8_str_new(str.as_ptr() as *const _, str.len() as i64) }

yjit/src/invariants.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ pub fn track_no_ep_escape_assumption(uninit_block: BlockRef, iseq: IseqPtr) {
168168
.insert(uninit_block);
169169
}
170170

171-
/// Returns true if a given ISEQ has previously escaped an environment.
172-
pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
171+
/// Returns true if a given ISEQ has escaped an environment since YJIT boot.
172+
pub fn seen_escaped_env(iseq: IseqPtr) -> bool {
173173
Invariants::get_instance()
174174
.no_ep_escape_iseqs
175175
.get(&iseq)

yjit/src/stats.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,8 @@ make_counters! {
601601
iseq_stack_too_large,
602602
iseq_too_long,
603603

604+
exceptional_entry_escaped_env,
605+
604606
temp_reg_opnd,
605607
temp_mem_opnd,
606608
temp_spill,

yjit/src/yjit.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,13 @@ pub extern "C" fn rb_yjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc
162162
return std::ptr::null();
163163
}
164164

165+
// In case of exceptional entry, reject escaped environment.
166+
// This allows us to use the fact that new frames generally start with an on-stack environment.
167+
if jit_exception && unsafe { cfp_env_has_escaped(get_ec_cfp(ec)) } {
168+
incr_counter!(exceptional_entry_escaped_env);
169+
return std::ptr::null();
170+
}
171+
165172
// If a custom call threshold was not specified on the command-line and
166173
// this is a large application (has very many ISEQs), switch to
167174
// using the call threshold for large applications after this entry point

0 commit comments

Comments
 (0)