Files
Takashi Kokubun e4beaa2c9c merge revision(s) 9efe557da5, 53f7189ff9: [Backport #22224]
[PATCH] ZJIT: Drop has_blockiseq guard on local variable access (#16113)

	So `has_blockiseq` was masking two other bugs.

	First, that we had only a static EP escape check for an iseq. We need to check
	both that an iseq is not known to statically escape them *and that it has not
	escaped them before*. Only then can we use SSA locals. This popped up in a
	weird PP unit test that was reducible to the test cases added in the PR. So I
	fixed that.

	Second, that we didn't VM lock when code patching for EP escape. That showed up
	in the Ractor btests in my previous PRs #14600 and #15254 #15316, and I had no
	idea what was going on at the time, so I dropped it. But if we add this lock,
	like all the other invariant patching cases have, the problem goes away. Seems
	reasonable.

	Finally, we can use normal full SSA locals even in the presence of a blockiseq.
	We still reload all of them after each send-with-block, but we can in a later
	PR reduce that to just the locals that are (recursively) visibly written to.

	[PATCH] [Bug #22224] Ractors, YJIT: Skip re-reporting recorded EP escapes to the JITs (#18176)

	* [22224] Ractors, YJIT: Skip re-reporting recorded EP escapes to the JITs

	vm_make_env_each reports every EP escape, and both
	rb_yjit_invalidate_ep_is_bp and rb_zjit_invalidate_no_ep_escape take
	the VM lock with a stop-all-Ractors barrier on every call. For a given
	iseq only the first report does anything: afterwards YJIT's
	no_ep_escape_iseqs entry is permanently an empty set and ZJIT's
	ep_escape_iseqs insert is a no-op, so the barrier is paid for nothing.
	Under multiple Ractors this serializes the whole process on every Proc
	created from an already-escaped iseq: a workload creating one lambda
	per iteration runs 150x slower on 8 Ractors than on 1 with YJIT
	enabled.

	Record the report in a jit_ep_escape_recorded flag on the iseq body,
	set by the active JIT while it holds the VM lock, and skip the calls
	in vm_make_env_each once it is set. The flag sits in existing padding
	and the body is ZALLOC'd, so it starts false and dies with the iseq.
	Setting it in the JIT rather than in the caller keeps mid-process
	RubyVM::YJIT.enable correct: escapes reported while no JIT is
	initialized are recorded by nobody and must not set the flag. The
	unlocked read is benign: a stale false causes one redundant,
	idempotent report.

	On the micro-benchmark from the bug, --yjit at 8 Ractors goes from
	0.04M to 11.07M iterations/s (277x).

	* [22224] Deduplicate the EP escape record onto the iseq body

	The jit_ep_escape_recorded flag introduced in the previous commit is
	now the JITs' only record of an EP escape: YJIT's no_ep_escape_iseqs
	keeps only iseqs with live assumptions (the empty-set escape markers
	are gone), and ZJIT's ep_escape_iseqs set is removed together with its
	update_ep_escape_iseqs compaction pass, since the flag lives in the
	iseq body, which the GC does not move. Both JITs read the flag back at
	compile time through a new rb_jit_iseq_ep_escape_recorded_p helper.

	Co-authored-by: Alan Wu <XrXr@users.noreply.github.com>

Only the two correctness changes of 9efe557da5 are backported: the runtime
EP-escape check in iseq_to_hir() and the VM lock in
rb_zjit_invalidate_no_ep_escape().  Without the former, suppressing repeat
escape reports would leave NoEPEscape patch points registered after an
escape never invalidated, because ZJIT here registers them unconditionally.
Its has_blockiseq removal is an optimization and is left out; keeping
has_blockiseq is strictly more conservative.

ZJIT here keeps the name invariants::iseq_escapes_ep() rather than renaming
it to iseq_seen_ep_escape(), since e0184fff1a is not backported.
2026-09-14 15:48:53 -07:00
..