Skip to content

Commit b2ee1bb

Browse files
committed
ZJIT: Emit polymorphic getinstancevariable
1 parent adb9bbd commit b2ee1bb

2 files changed

Lines changed: 105 additions & 4 deletions

File tree

zjit/src/hir.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6728,7 +6728,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
67286728
// Keep compiling blocks until the queue becomes empty
67296729
let mut visited = HashSet::new();
67306730
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
6731-
while let Some((incoming_state, block, mut insn_idx, mut local_inval)) = queue.pop_front() {
6731+
while let Some((incoming_state, mut block, mut insn_idx, mut local_inval)) = queue.pop_front() {
67326732
// Compile each block only once
67336733
if visited.contains(&block) { continue; }
67346734
visited.insert(block);
@@ -7899,8 +7899,44 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
78997899
fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) });
79007900
break; // End the block
79017901
}
7902-
let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id });
7903-
state.stack_push(result);
7902+
if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_state.insn_idx) {
7903+
self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id });
7904+
let shape = fun.load_shape(block, self_param);
7905+
let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx));
7906+
let join_param = fun.push_insn(join_block, Insn::Param);
7907+
// Dedup by expected shape so objects with different classes but the same shape can share code
7908+
let mut seen_shapes = Vec::with_capacity(summary.buckets().len());
7909+
for &profiled_type in summary.buckets() {
7910+
// End of the buckets
7911+
if profiled_type.is_empty() { break; }
7912+
// Instance variable lookups on immediate values are always nil; don't bother
7913+
if profiled_type.flags().is_immediate() { continue; }
7914+
let expected_shape = profiled_type.shape();
7915+
assert!(expected_shape.is_valid());
7916+
if seen_shapes.contains(&expected_shape) { continue; }
7917+
seen_shapes.push(expected_shape);
7918+
let expected_shape_const = fun.push_insn(block, Insn::Const { val: Const::CShape(expected_shape) });
7919+
let has_shape = fun.push_insn(block, Insn::IsBitEqual { left: shape, right: expected_shape_const });
7920+
let iftrue_block = fun.new_block(insn_idx);
7921+
let target = BranchEdge { target: iftrue_block, args: vec![] };
7922+
fun.push_insn(block, Insn::IfTrue { val: has_shape, target });
7923+
let result = fun.load_ivar(iftrue_block, self_param, profiled_type, id, exit_id);
7924+
fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] }));
7925+
}
7926+
// In the fallthrough case, do a generic interpreter getivar and then join.
7927+
let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id });
7928+
fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] }));
7929+
state.stack_push(join_param);
7930+
// Continue compilation from the join block at the next instruction.
7931+
// Make a copy of the current state without the args (pop the receiver
7932+
// and push the result) because we just use the locals/stack sizes to
7933+
// make the right number of Params
7934+
block = join_block;
7935+
} else {
7936+
// Possibly monomorphic case; handled in optimize_getivar
7937+
let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id });
7938+
state.stack_push(result);
7939+
}
79047940
}
79057941
YARVINSN_setinstancevariable => {
79067942
let id = ID(get_arg(pc, 0).as_u64());

zjit/src/hir/opt_tests.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7443,7 +7443,72 @@ mod hir_opt_tests {
74437443
}
74447444

74457445
#[test]
7446-
fn test_dont_optimize_getivar_polymorphic() {
7446+
fn test_optimize_getivar_polymorphic() {
7447+
set_call_threshold(3);
7448+
eval(r#"
7449+
class C
7450+
def foo_then_bar
7451+
@foo = 1
7452+
@bar = 2
7453+
end
7454+
7455+
def bar_then_foo
7456+
1000.times { |i| instance_variable_set(:"@v#{i}", i) }
7457+
@bar = 3
7458+
@foo = 4
7459+
end
7460+
7461+
def foo = @foo + 1
7462+
end
7463+
7464+
O1 = C.new
7465+
O1.foo_then_bar
7466+
O2 = C.new
7467+
O2.bar_then_foo
7468+
O1.foo
7469+
O2.foo
7470+
"#);
7471+
assert_snapshot!(hir_string_proc("C.instance_method(:foo)"), @"
7472+
fn foo@<compiled>:14:
7473+
bb1():
7474+
EntryPoint interpreter
7475+
v1:BasicObject = LoadSelf
7476+
Jump bb3(v1)
7477+
bb2():
7478+
EntryPoint JIT(0)
7479+
v4:BasicObject = LoadArg :self@0
7480+
Jump bb3(v4)
7481+
bb3(v6:BasicObject):
7482+
PatchPoint SingleRactorMode
7483+
v11:HeapBasicObject = GuardType v6, HeapBasicObject
7484+
v12:CShape = LoadField v11, :_shape_id@0x1000
7485+
v14:CShape[0x1001] = Const CShape(0x1001)
7486+
v15:CBool = IsBitEqual v12, v14
7487+
IfTrue v15, bb5()
7488+
v20:CShape[0x1002] = Const CShape(0x1002)
7489+
v21:CBool = IsBitEqual v12, v20
7490+
IfTrue v21, bb6()
7491+
v25:BasicObject = GetIvar v11, :@foo
7492+
Jump bb4(v25)
7493+
bb5():
7494+
v17:CPtr = LoadField v11, :_as_heap@0x1003
7495+
v18:BasicObject = LoadField v17, :@foo@0x1004
7496+
Jump bb4(v18)
7497+
bb6():
7498+
v23:BasicObject = LoadField v11, :@foo@0x1003
7499+
Jump bb4(v23)
7500+
bb4(v13:BasicObject):
7501+
v28:Fixnum[1] = Const Value(1)
7502+
PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018)
7503+
v39:Fixnum = GuardType v13, Fixnum
7504+
v40:Fixnum = FixnumAdd v39, v28
7505+
CheckInterrupts
7506+
Return v40
7507+
");
7508+
}
7509+
7510+
#[test]
7511+
fn test_dont_optimize_attr_accessor_polymorphic() {
74477512
set_call_threshold(3);
74487513
eval("
74497514
class C

0 commit comments

Comments
 (0)