Skip to content

Commit ef3c3e6

Browse files
authored
ZJIT: Stop optimizing toplevel locals (ruby#14458)
1 parent c06e704 commit ef3c3e6

File tree

5 files changed

+84
-23
lines changed

5 files changed

+84
-23
lines changed

.github/workflows/zjit-macos.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ jobs:
4747
configure: '--enable-zjit=dev'
4848
zjit_opts: '--zjit-call-threshold=1'
4949

50+
- test_task: 'test-spec'
51+
configure: '--enable-zjit=dev'
52+
specopts: '-T --zjit-call-threshold=1'
53+
5054
env:
5155
GITPULLOPTIONS: --no-tags origin ${{ github.ref }}
5256
RUN_OPTS: ${{ matrix.zjit_opts }}

.github/workflows/zjit-ubuntu.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ jobs:
6868
configure: '--enable-zjit=dev'
6969
zjit_opts: '--zjit-call-threshold=1'
7070

71+
- test_task: 'test-spec'
72+
configure: '--enable-zjit=dev'
73+
specopts: '-T --zjit-call-threshold=1'
74+
7175
env:
7276
GITPULLOPTIONS: --no-tags origin ${{ github.ref }}
7377
RUN_OPTS: ${{ matrix.zjit_opts }}

test/ruby/test_zjit.rb

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,32 @@ def test
196196
}, insns: [:setglobal]
197197
end
198198

199+
def test_toplevel_binding
200+
# Not using assert_compiles, which doesn't use the toplevel frame for `test_script`.
201+
out, err, status = eval_with_jit(%q{
202+
a = 1
203+
b = 2
204+
TOPLEVEL_BINDING.local_variable_set(:b, 3)
205+
c = 4
206+
print [a, b, c]
207+
})
208+
assert_success(out, err, status)
209+
assert_equal "[1, 3, 4]", out
210+
end
211+
212+
def test_toplevel_local_after_eval
213+
# Not using assert_compiles, which doesn't use the toplevel frame for `test_script`.
214+
out, err, status = eval_with_jit(%q{
215+
a = 1
216+
b = 2
217+
eval('b = 3')
218+
c = 4
219+
print [a, b, c]
220+
})
221+
assert_success(out, err, status)
222+
assert_equal "[1, 3, 4]", out
223+
end
224+
199225
def test_getlocal_after_eval
200226
assert_compiles '2', %q{
201227
def test
@@ -2428,12 +2454,8 @@ def assert_runs(expected, test_script, insns: [], assert_compiles: false, **opts
24282454
IO.open(#{pipe_fd}).write(Marshal.dump(result))
24292455
RUBY
24302456

2431-
status, out, err, result = eval_with_jit(script, pipe_fd:, **opts)
2432-
2433-
message = "exited with status #{status.to_i}"
2434-
message << "\nstdout:\n```\n#{out}```\n" unless out.empty?
2435-
message << "\nstderr:\n```\n#{err}```\n" unless err.empty?
2436-
assert status.success?, message
2457+
out, err, status, result = eval_with_jit(script, pipe_fd:, **opts)
2458+
assert_success(out, err, status)
24372459

24382460
result = Marshal.load(result)
24392461
assert_equal(expected, result.fetch(:ret_val).inspect)
@@ -2462,7 +2484,7 @@ def eval_with_jit(
24622484
debug: true,
24632485
allowed_iseqs: nil,
24642486
timeout: 1000,
2465-
pipe_fd:
2487+
pipe_fd: nil
24662488
)
24672489
args = ["--disable-gems"]
24682490
if zjit
@@ -2478,18 +2500,25 @@ def eval_with_jit(
24782500
end
24792501
end
24802502
args << "-e" << script_shell_encode(script)
2481-
pipe_r, pipe_w = IO.pipe
2482-
# Separate thread so we don't deadlock when
2483-
# the child ruby blocks writing the output to pipe_fd
2484-
pipe_out = nil
2485-
pipe_reader = Thread.new do
2486-
pipe_out = pipe_r.read
2487-
pipe_r.close
2503+
ios = {}
2504+
if pipe_fd
2505+
pipe_r, pipe_w = IO.pipe
2506+
# Separate thread so we don't deadlock when
2507+
# the child ruby blocks writing the output to pipe_fd
2508+
pipe_out = nil
2509+
pipe_reader = Thread.new do
2510+
pipe_out = pipe_r.read
2511+
pipe_r.close
2512+
end
2513+
ios[pipe_fd] = pipe_w
24882514
end
2489-
out, err, status = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios: { pipe_fd => pipe_w })
2490-
pipe_w.close
2491-
pipe_reader.join(timeout)
2492-
[status, out, err, pipe_out]
2515+
result = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios:)
2516+
if pipe_fd
2517+
pipe_w.close
2518+
pipe_reader.join(timeout)
2519+
result << pipe_out
2520+
end
2521+
result
24932522
ensure
24942523
pipe_reader&.kill
24952524
pipe_reader&.join(timeout)
@@ -2498,6 +2527,13 @@ def eval_with_jit(
24982527
jitlist&.unlink
24992528
end
25002529

2530+
def assert_success(out, err, status)
2531+
message = "exited with status #{status.to_i}"
2532+
message << "\nstdout:\n```\n#{out}```\n" unless out.empty?
2533+
message << "\nstderr:\n```\n#{err}```\n" unless err.empty?
2534+
assert status.success?, message
2535+
end
2536+
25012537
def script_shell_encode(s)
25022538
# We can't pass utf-8-encoded characters directly in a shell arg. But we can use Ruby \u constants.
25032539
s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join

zjit/src/cruby.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,17 @@ pub fn iseq_opcode_at_idx(iseq: IseqPtr, insn_idx: u32) -> u32 {
294294
unsafe { rb_iseq_opcode_at_pc(iseq, pc) as u32 }
295295
}
296296

297+
/// Return true if the ISEQ always uses a frame with escaped EP.
298+
pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
299+
match unsafe { get_iseq_body_type(iseq) } {
300+
// The EP of the <main> frame points to TOPLEVEL_BINDING
301+
ISEQ_TYPE_MAIN |
302+
// eval frames point to the EP of another frame or scope
303+
ISEQ_TYPE_EVAL => true,
304+
_ => false,
305+
}
306+
}
307+
297308
/// Iterate over all existing ISEQs
298309
pub fn for_each_iseq<F: FnMut(IseqPtr)>(mut callback: F) {
299310
unsafe extern "C" fn callback_wrapper(iseq: IseqPtr, data: *mut c_void) {

zjit/src/hir.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
30193019
let payload = get_or_create_iseq_payload(iseq);
30203020
let mut profiles = ProfileOracle::new(payload);
30213021
let mut fun = Function::new(iseq);
3022+
30223023
// Compute a map of PC->Block by finding jump targets
30233024
let BytecodeInfo { jump_targets, has_blockiseq } = compute_bytecode_info(iseq);
30243025
let mut insn_idx_to_block = HashMap::new();
@@ -3029,6 +3030,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
30293030
insn_idx_to_block.insert(insn_idx, fun.new_block(insn_idx));
30303031
}
30313032

3033+
// Check if the EP is escaped for the ISEQ from the beginning. We give up
3034+
// optimizing locals in that case because they're shared with other frames.
3035+
let ep_escaped = iseq_escapes_ep(iseq);
3036+
30323037
// Iteratively fill out basic blocks using a queue
30333038
// TODO(max): Basic block arguments at edges
30343039
let mut queue = std::collections::VecDeque::new();
@@ -3066,7 +3071,6 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
30663071
let mut visited = HashSet::new();
30673072

30683073
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
3069-
let iseq_type = unsafe { get_iseq_body_type(iseq) };
30703074
while let Some((incoming_state, block, mut insn_idx, mut local_inval)) = queue.pop_front() {
30713075
if visited.contains(&block) { continue; }
30723076
visited.insert(block);
@@ -3361,8 +3365,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
33613365
}
33623366
YARVINSN_getlocal_WC_0 => {
33633367
let ep_offset = get_arg(pc, 0).as_u32();
3364-
if iseq_type == ISEQ_TYPE_EVAL || has_blockiseq {
3365-
// On eval, the locals are always on the heap, so read the local using EP.
3368+
if ep_escaped || has_blockiseq { // TODO: figure out how to drop has_blockiseq here
3369+
// Read the local using EP
33663370
let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0 });
33673371
state.setlocal(ep_offset, val); // remember the result to spill on side-exits
33683372
state.stack_push(val);
@@ -3374,15 +3378,16 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
33743378
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoEPEscape(iseq), state: exit_id });
33753379
local_inval = false;
33763380
}
3381+
// Read the local from FrameState
33773382
let val = state.getlocal(ep_offset);
33783383
state.stack_push(val);
33793384
}
33803385
}
33813386
YARVINSN_setlocal_WC_0 => {
33823387
let ep_offset = get_arg(pc, 0).as_u32();
33833388
let val = state.stack_pop()?;
3384-
if iseq_type == ISEQ_TYPE_EVAL || has_blockiseq {
3385-
// On eval, the locals are always on the heap, so write the local using EP.
3389+
if ep_escaped || has_blockiseq { // TODO: figure out how to drop has_blockiseq here
3390+
// Write the local using EP
33863391
fun.push_insn(block, Insn::SetLocal { val, ep_offset, level: 0 });
33873392
} else if local_inval {
33883393
// If there has been any non-leaf call since JIT entry or the last patch point,
@@ -3391,6 +3396,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
33913396
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoEPEscape(iseq), state: exit_id });
33923397
local_inval = false;
33933398
}
3399+
// Write the local into FrameState
33943400
state.setlocal(ep_offset, val);
33953401
}
33963402
YARVINSN_getlocal_WC_1 => {

0 commit comments

Comments
 (0)