Skip to content

Commit 9aed9e9

Browse files
authored
ZJIT: Fold Send into SendWithoutBlockDirect if we know the class statically (ruby#13172)
Fold Send into SendWithoutBlockDirect if we know the class statically This applies for constants and also for values where we know the type for other reasons.
1 parent 71166f6 commit 9aed9e9

File tree

2 files changed

+74
-9
lines changed

2 files changed

+74
-9
lines changed

zjit/src/hir.rs

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -978,18 +978,22 @@ impl Function {
978978
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumGt { left, right }, BOP_GT, self_val, args[0], payload, state),
979979
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == ">=" && args.len() == 1 =>
980980
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumGe { left, right }, BOP_GE, self_val, args[0], payload, state),
981-
Insn::SendWithoutBlock { self_val, call_info, cd, args, state } => {
981+
Insn::SendWithoutBlock { mut self_val, call_info, cd, args, state } => {
982982
let frame_state = self.frame_state(state);
983-
let self_type = match payload.get_operand_types(frame_state.insn_idx) {
984-
Some([self_type, ..]) if self_type.is_top_self() => self_type,
985-
_ => { self.push_insn_id(block, insn_id); continue; }
983+
let (klass, guard_equal_to) = if let Some(klass) = self.type_of(self_val).runtime_exact_ruby_class() {
984+
// If we know the class statically, use it to fold the lookup at compile-time.
985+
(klass, None)
986+
} else {
987+
// If we know that self is top-self from profile information, guard and use it to fold the lookup at compile-time.
988+
match payload.get_operand_types(frame_state.insn_idx) {
989+
Some([self_type, ..]) if self_type.is_top_self() => (self_type.exact_ruby_class().unwrap(), self_type.ruby_object()),
990+
_ => { self.push_insn_id(block, insn_id); continue; }
991+
}
986992
};
987-
let top_self = self_type.ruby_object().unwrap();
988-
let top_self_klass = top_self.class_of();
989993
let ci = unsafe { get_call_data_ci(cd) }; // info about the call site
990994
let mid = unsafe { vm_ci_mid(ci) };
991995
// Do method lookup
992-
let mut cme = unsafe { rb_callable_method_entry(top_self_klass, mid) };
996+
let mut cme = unsafe { rb_callable_method_entry(klass, mid) };
993997
if cme.is_null() {
994998
self.push_insn_id(block, insn_id); continue;
995999
}
@@ -998,11 +1002,14 @@ impl Function {
9981002
cme = unsafe { rb_check_overloaded_cme(cme, ci) };
9991003
let def_type = unsafe { get_cme_def_type(cme) };
10001004
if def_type != VM_METHOD_TYPE_ISEQ {
1005+
// TODO(max): Allow non-iseq; cache cme
10011006
self.push_insn_id(block, insn_id); continue;
10021007
}
1003-
self.push_insn(block, Insn::PatchPoint(Invariant::MethodRedefined { klass: top_self_klass, method: mid }));
1008+
self.push_insn(block, Insn::PatchPoint(Invariant::MethodRedefined { klass, method: mid }));
10041009
let iseq = unsafe { get_def_iseq_ptr((*cme).def) };
1005-
let self_val = self.push_insn(block, Insn::GuardBitEquals { val: self_val, expected: top_self, state });
1010+
if let Some(expected) = guard_equal_to {
1011+
self_val = self.push_insn(block, Insn::GuardBitEquals { val: self_val, expected, state });
1012+
}
10061013
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, call_info, cd, iseq, args, state });
10071014
self.make_equal_to(insn_id, send_direct);
10081015
}
@@ -1038,6 +1045,9 @@ impl Function {
10381045
// TODO(alan): there was a seemingly a miscomp here if you swap with
10391046
// `inexact_ruby_class`. Theoretically it can call a method too general
10401047
// for the receiver. Confirm and add a test.
1048+
//
1049+
// TODO(max): Use runtime_exact_ruby_class so we can also specialize on known (not just
1050+
// profiled) types.
10411051
let (recv_class, recv_type) = payload.get_operand_types(iseq_insn_idx)
10421052
.and_then(|types| types.get(argc as usize))
10431053
.and_then(|recv_type| recv_type.exact_ruby_class().and_then(|class| Some((class, recv_type))))
@@ -3354,6 +3364,41 @@ mod opt_tests {
33543364
"#]]);
33553365
}
33563366

3367+
#[test]
3368+
fn const_send_direct_integer() {
3369+
eval("
3370+
def test(x) = 1.zero?
3371+
");
3372+
assert_optimized_method_hir("test", expect![[r#"
3373+
fn test:
3374+
bb0(v0:BasicObject):
3375+
v2:Fixnum[1] = Const Value(1)
3376+
PatchPoint MethodRedefined(Integer@0x1000, zero?@0x1008)
3377+
v7:BasicObject = SendWithoutBlockDirect v2, :zero? (0x1010)
3378+
Return v7
3379+
"#]]);
3380+
}
3381+
3382+
#[test]
3383+
fn class_known_send_direct_array() {
3384+
eval("
3385+
def test(x)
3386+
a = [1,2,3]
3387+
a.first
3388+
end
3389+
");
3390+
assert_optimized_method_hir("test", expect![[r#"
3391+
fn test:
3392+
bb0(v0:BasicObject):
3393+
v1:NilClassExact = Const Value(nil)
3394+
v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
3395+
v5:ArrayExact = ArrayDup v3
3396+
PatchPoint MethodRedefined(Array@0x1008, first@0x1010)
3397+
v10:BasicObject = SendWithoutBlockDirect v5, :first (0x1018)
3398+
Return v10
3399+
"#]]);
3400+
}
3401+
33573402
#[test]
33583403
fn string_bytesize_simple() {
33593404
eval("

zjit/src/hir_type/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,26 @@ impl Type {
364364
}
365365
}
366366

367+
/// Return a pointer to the Ruby class that an object of this Type would have at run-time, if
368+
/// known. This includes classes for HIR types such as ArrayExact or NilClassExact, which have
369+
/// canonical Type representations that lack an explicit specialization in their `spec` fields.
370+
pub fn runtime_exact_ruby_class(&self) -> Option<VALUE> {
371+
if let Some(val) = self.exact_ruby_class() {
372+
return Some(val);
373+
}
374+
if self.is_subtype(types::ArrayExact) { return Some(unsafe { rb_cArray }); }
375+
if self.is_subtype(types::FalseClassExact) { return Some(unsafe { rb_cFalseClass }); }
376+
if self.is_subtype(types::FloatExact) { return Some(unsafe { rb_cFloat }); }
377+
if self.is_subtype(types::HashExact) { return Some(unsafe { rb_cHash }); }
378+
if self.is_subtype(types::IntegerExact) { return Some(unsafe { rb_cInteger }); }
379+
if self.is_subtype(types::NilClassExact) { return Some(unsafe { rb_cNilClass }); }
380+
if self.is_subtype(types::ObjectExact) { return Some(unsafe { rb_cObject }); }
381+
if self.is_subtype(types::StringExact) { return Some(unsafe { rb_cString }); }
382+
if self.is_subtype(types::SymbolExact) { return Some(unsafe { rb_cSymbol }); }
383+
if self.is_subtype(types::TrueClassExact) { return Some(unsafe { rb_cTrueClass }); }
384+
None
385+
}
386+
367387
/// Check bit equality of two `Type`s. Do not use! You are probably looking for [`Type::is_subtype`].
368388
pub fn bit_equal(&self, other: Type) -> bool {
369389
self.bits == other.bits && self.spec == other.spec

0 commit comments

Comments
 (0)