Skip to content

Commit 797a411

Browse files
authored
ZJIT: Support variadic C calls (ruby#14575)
* ZJIT: Support variadic C calls This reduces the `dynamic_send_count` in `liquid-render` by ~21% * ZJIT: Reuse gen_push_frame * ZJIT: Avoid optimizing variadic C call when tracing is enabled
1 parent ff5105f commit 797a411

File tree

6 files changed

+201
-8
lines changed

6 files changed

+201
-8
lines changed

test/ruby/test_zjit.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2710,6 +2710,44 @@ def test
27102710
}, insns: [:invokeblock]
27112711
end
27122712

2713+
def test_ccall_variadic_with_multiple_args
2714+
assert_compiles "[1, 2, 3]", %q{
2715+
def test
2716+
a = []
2717+
a.push(1, 2, 3)
2718+
a
2719+
end
2720+
2721+
test
2722+
test
2723+
}, insns: [:opt_send_without_block]
2724+
end
2725+
2726+
def test_ccall_variadic_with_no_args
2727+
assert_compiles "[1]", %q{
2728+
def test
2729+
a = [1]
2730+
a.push
2731+
end
2732+
2733+
test
2734+
test
2735+
}, insns: [:opt_send_without_block]
2736+
end
2737+
2738+
def test_ccall_variadic_with_no_args_causing_argument_error
2739+
assert_compiles ":error", %q{
2740+
def test
2741+
format
2742+
rescue ArgumentError
2743+
:error
2744+
end
2745+
2746+
test
2747+
test
2748+
}, insns: [:opt_send_without_block]
2749+
end
2750+
27132751
private
27142752

27152753
# Assert that every method call in `test_script` can be compiled by ZJIT

zjit.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,23 @@ rb_zjit_defined_ivar(VALUE obj, ID id, VALUE pushval)
158158
return result ? pushval : Qnil;
159159
}
160160

161+
bool
162+
rb_zjit_method_tracing_currently_enabled(void)
163+
{
164+
rb_event_flag_t tracing_events;
165+
if (rb_multi_ractor_p()) {
166+
tracing_events = ruby_vm_event_enabled_global_flags;
167+
}
168+
else {
169+
// At the time of writing, events are never removed from
170+
// ruby_vm_event_enabled_global_flags so always checking using it would
171+
// mean we don't compile even after tracing is disabled.
172+
tracing_events = rb_ec_ractor_hooks(GET_EC())->events;
173+
}
174+
175+
return tracing_events & (RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN);
176+
}
177+
161178
bool
162179
rb_zjit_insn_leaf(int insn, const VALUE *opes)
163180
{

zjit/bindgen/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ fn main() {
274274
.allowlist_function("rb_zjit_local_id")
275275
.allowlist_function("rb_set_cfp_(pc|sp)")
276276
.allowlist_function("rb_c_method_tracing_currently_enabled")
277+
.allowlist_function("rb_zjit_method_tracing_currently_enabled")
277278
.allowlist_function("rb_full_cfunc_return")
278279
.allowlist_function("rb_assert_(iseq|cme)_handle")
279280
.allowlist_function("rb_IMEMO_TYPE_P")

zjit/src/codegen.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
397397
&Insn::GuardBlockParamProxy { level, state } => no_output!(gen_guard_block_param_proxy(jit, asm, level, &function.frame_state(state))),
398398
Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))),
399399
Insn::CCall { cfun, args, name: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfun, opnds!(args)),
400+
Insn::CCallVariadic { cfun, recv, args, name: _, cme, state } => {
401+
gen_ccall_variadic(jit, asm, *cfun, opnd!(recv), opnds!(args), *cme, &function.frame_state(*state))
402+
}
400403
Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id),
401404
Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))),
402405
Insn::GetGlobal { id, state } => gen_getglobal(jit, asm, *id, &function.frame_state(*state)),
@@ -638,6 +641,52 @@ fn gen_ccall(asm: &mut Assembler, cfun: *const u8, args: Vec<Opnd>) -> lir::Opnd
638641
asm.ccall(cfun, args)
639642
}
640643

644+
/// Generate code for a variadic C function call
645+
/// func(int argc, VALUE *argv, VALUE recv)
646+
fn gen_ccall_variadic(
647+
jit: &mut JITState,
648+
asm: &mut Assembler,
649+
cfun: *const u8,
650+
recv: Opnd,
651+
args: Vec<Opnd>,
652+
cme: *const rb_callable_method_entry_t,
653+
state: &FrameState,
654+
) -> lir::Opnd {
655+
gen_prepare_non_leaf_call(jit, asm, state);
656+
657+
gen_push_frame(asm, args.len(), state, ControlFrame {
658+
recv,
659+
iseq: None,
660+
cme,
661+
frame_type: VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL,
662+
});
663+
664+
asm_comment!(asm, "switch to new SP register");
665+
let sp_offset = (state.stack().len() - args.len() + VM_ENV_DATA_SIZE.as_usize()) * SIZEOF_VALUE;
666+
let new_sp = asm.add(SP, sp_offset.into());
667+
asm.mov(SP, new_sp);
668+
669+
asm_comment!(asm, "switch to new CFP");
670+
let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
671+
asm.mov(CFP, new_cfp);
672+
asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
673+
674+
let argv_ptr = gen_push_opnds(jit, asm, &args);
675+
let result = asm.ccall(cfun, vec![args.len().into(), argv_ptr, recv]);
676+
gen_pop_opnds(asm, &args);
677+
678+
asm_comment!(asm, "pop C frame");
679+
let new_cfp = asm.add(CFP, RUBY_SIZEOF_CONTROL_FRAME.into());
680+
asm.mov(CFP, new_cfp);
681+
asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
682+
683+
asm_comment!(asm, "restore SP register for the caller");
684+
let new_sp = asm.sub(SP, sp_offset.into());
685+
asm.mov(SP, new_sp);
686+
687+
result
688+
}
689+
641690
/// Emit an uncached instance variable lookup
642691
fn gen_getivar(asm: &mut Assembler, recv: Opnd, id: ID) -> Opnd {
643692
asm_ccall!(asm, rb_ivar_get, recv, id.0.into())
@@ -1054,7 +1103,7 @@ fn gen_send_without_block_direct(
10541103
// TODO: Lazily materialize caller frames on side exits or when needed
10551104
gen_push_frame(asm, args.len(), state, ControlFrame {
10561105
recv,
1057-
iseq,
1106+
iseq: Some(iseq),
10581107
cme,
10591108
frame_type: VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL,
10601109
});
@@ -1582,7 +1631,7 @@ fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, state: &FrameS
15821631
/// Frame metadata written by gen_push_frame()
15831632
struct ControlFrame {
15841633
recv: Opnd,
1585-
iseq: IseqPtr,
1634+
iseq: Option<IseqPtr>,
15861635
cme: *const rb_callable_method_entry_t,
15871636
frame_type: u32,
15881637
}
@@ -1594,7 +1643,11 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C
15941643
// See vm_push_frame() for details
15951644
asm_comment!(asm, "push cme, specval, frame type");
15961645
// ep[-2]: cref of cme
1597-
let local_size = unsafe { get_iseq_body_local_table_size(frame.iseq) } as i32;
1646+
let local_size = if let Some(iseq) = frame.iseq {
1647+
(unsafe { get_iseq_body_local_table_size(iseq) }) as i32
1648+
} else {
1649+
0
1650+
};
15981651
let ep_offset = state.stack().len() as i32 + local_size - argc as i32 + VM_ENV_DATA_SIZE as i32 - 1;
15991652
asm.store(Opnd::mem(64, SP, (ep_offset - 2) * SIZEOF_VALUE_I32), VALUE::from(frame.cme).into());
16001653
// ep[-1]: block_handler or prev EP
@@ -1609,9 +1662,19 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C
16091662
}
16101663

16111664
asm_comment!(asm, "push callee control frame");
1612-
// cfp_opnd(RUBY_OFFSET_CFP_PC): written by the callee frame on side-exits or non-leaf calls
1613-
// cfp_opnd(RUBY_OFFSET_CFP_SP): written by the callee frame on side-exits or non-leaf calls
1614-
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_ISEQ), VALUE::from(frame.iseq).into());
1665+
1666+
if let Some(iseq) = frame.iseq {
1667+
// cfp_opnd(RUBY_OFFSET_CFP_PC): written by the callee frame on side-exits or non-leaf calls
1668+
// cfp_opnd(RUBY_OFFSET_CFP_SP): written by the callee frame on side-exits or non-leaf calls
1669+
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_ISEQ), VALUE::from(iseq).into());
1670+
} else {
1671+
// C frames don't have a PC and ISEQ
1672+
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_PC), 0.into());
1673+
let new_sp = asm.lea(Opnd::mem(64, SP, (ep_offset + 1) * SIZEOF_VALUE_I32));
1674+
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_SP), new_sp);
1675+
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_ISEQ), 0.into());
1676+
}
1677+
16151678
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_SELF), frame.recv);
16161679
let ep = asm.lea(Opnd::mem(64, SP, ep_offset * SIZEOF_VALUE_I32));
16171680
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_EP), ep);

zjit/src/cruby_bindings.inc.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zjit/src/hir.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,17 @@ pub enum Insn {
621621
/// `name` is for printing purposes only
622622
CCall { cfun: *const u8, args: Vec<InsnId>, name: ID, return_type: Type, elidable: bool },
623623

624+
/// Call a variadic C function with signature: func(int argc, VALUE *argv, VALUE recv)
625+
/// This handles frame setup, argv creation, and frame teardown all in one
626+
CCallVariadic {
627+
cfun: *const u8,
628+
recv: InsnId,
629+
args: Vec<InsnId>,
630+
cme: *const rb_callable_method_entry_t,
631+
name: ID,
632+
state: InsnId,
633+
},
634+
624635
/// Un-optimized fallback implementation (dynamic dispatch) for send-ish instructions
625636
/// Ignoring keyword arguments etc for now
626637
SendWithoutBlock {
@@ -940,6 +951,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
940951
}
941952
Ok(())
942953
},
954+
Insn::CCallVariadic { cfun, recv, args, name, .. } => {
955+
write!(f, "CCallVariadic {}@{:p}, {recv}", name.contents_lossy(), self.ptr_map.map_ptr(cfun))?;
956+
for arg in args {
957+
write!(f, ", {arg}")?;
958+
}
959+
Ok(())
960+
},
943961
Insn::Snapshot { state } => write!(f, "Snapshot {}", state.print(self.ptr_map)),
944962
Insn::Defined { op_type, v, .. } => {
945963
// op_type (enum defined_type) printing logic from iseq.c.
@@ -1422,6 +1440,9 @@ impl Function {
14221440
&ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state },
14231441
&ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) },
14241442
&CCall { cfun, ref args, name, return_type, elidable } => CCall { cfun, args: find_vec!(args), name, return_type, elidable },
1443+
&CCallVariadic { cfun, recv, ref args, cme, name, state } => CCallVariadic {
1444+
cfun, recv: find!(recv), args: find_vec!(args), cme, name, state
1445+
},
14251446
&Defined { op_type, obj, pushval, v, state } => Defined { op_type, obj, pushval, v: find!(v), state: find!(state) },
14261447
&DefinedIvar { self_val, pushval, id, state } => DefinedIvar { self_val: find!(self_val), pushval, id, state },
14271448
&NewArray { ref elements, state } => NewArray { elements: find_vec!(elements), state: find!(state) },
@@ -1509,6 +1530,7 @@ impl Function {
15091530
Insn::ObjectAlloc { .. } => types::HeapObject,
15101531
Insn::ObjectAllocClass { class, .. } => Type::from_class(*class),
15111532
Insn::CCall { return_type, .. } => *return_type,
1533+
Insn::CCallVariadic { .. } => types::BasicObject,
15121534
Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type),
15131535
Insn::GuardTypeNot { .. } => types::BasicObject,
15141536
Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_value(*expected)),
@@ -2076,9 +2098,39 @@ impl Function {
20762098
return Ok(());
20772099
}
20782100
}
2101+
// Variadic method
20792102
-1 => {
2080-
// (argc, argv, self) parameter form
2081-
// Falling through for now
2103+
if unsafe { rb_zjit_method_tracing_currently_enabled() } {
2104+
return Err(());
2105+
}
2106+
// The method gets a pointer to the first argument
2107+
// func(int argc, VALUE *argv, VALUE recv)
2108+
let ci_flags = unsafe { vm_ci_flag(call_info) };
2109+
if ci_flags & VM_CALL_ARGS_SIMPLE != 0 {
2110+
fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoTracePoint, state });
2111+
fun.push_insn(block, Insn::PatchPoint {
2112+
invariant: Invariant::MethodRedefined {
2113+
klass: recv_class,
2114+
method: method_id,
2115+
cme: method
2116+
},
2117+
state
2118+
});
2119+
2120+
let cfun = unsafe { get_mct_func(cfunc) }.cast();
2121+
let ccall = fun.push_insn(block, Insn::CCallVariadic {
2122+
cfun,
2123+
recv,
2124+
args,
2125+
cme: method,
2126+
name: method_id,
2127+
state,
2128+
});
2129+
2130+
fun.make_equal_to(send_insn_id, ccall);
2131+
return Ok(());
2132+
}
2133+
// Fall through for complex cases (splat, kwargs, etc.)
20822134
}
20832135
-2 => {
20842136
// (self, args_ruby_array) parameter form
@@ -2379,6 +2431,7 @@ impl Function {
23792431
}
23802432
&Insn::Send { recv, ref args, state, .. }
23812433
| &Insn::SendWithoutBlock { recv, ref args, state, .. }
2434+
| &Insn::CCallVariadic { recv, ref args, state, .. }
23822435
| &Insn::SendWithoutBlockDirect { recv, ref args, state, .. }
23832436
| &Insn::InvokeSuper { recv, ref args, state, .. } => {
23842437
worklist.push_back(recv);
@@ -6895,6 +6948,26 @@ mod opt_tests {
68956948
");
68966949
}
68976950

6951+
#[test]
6952+
fn test_optimize_variadic_ccall() {
6953+
eval("
6954+
def test
6955+
puts 'Hello'
6956+
end
6957+
test; test
6958+
");
6959+
assert_snapshot!(hir_string("test"), @r"
6960+
fn test@<compiled>:3:
6961+
bb0(v0:BasicObject):
6962+
v4:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
6963+
v6:StringExact = StringCopy v4
6964+
PatchPoint MethodRedefined(Object@0x1008, puts@0x1010, cme:0x1018)
6965+
v16:BasicObject = CCallVariadic puts@0x1040, v0, v6
6966+
CheckInterrupts
6967+
Return v16
6968+
");
6969+
}
6970+
68986971
#[test]
68996972
fn test_dont_optimize_fixnum_add_if_redefined() {
69006973
eval("

0 commit comments

Comments
 (0)