Skip to content

Commit 0cba3f8

Browse files
committed
ZJIT: Emit polymorphic getinstancevariable
1 parent 8db83fc commit 0cba3f8

2 files changed

Lines changed: 101 additions & 55 deletions

File tree

zjit/src/hir.rs

Lines changed: 39 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5845,33 +5845,6 @@ impl Function {
58455845
Ok(())
58465846
}
58475847

5848-
// Validate that every instruction use is from a block-local definition, which is a temporary
5849-
// constraint until we get a global register allocator.
5850-
// TODO(tenderworks): Remove this
5851-
fn temporary_validate_block_local_definite_assignment(&self) -> Result<(), ValidationError> {
5852-
for block in self.rpo() {
5853-
let mut assigned = InsnSet::with_capacity(self.insns.len());
5854-
for &param in &self.blocks[block.0].params {
5855-
assigned.insert(param);
5856-
}
5857-
// Check that each instruction's operands are assigned
5858-
for &insn_id in &self.blocks[block.0].insns {
5859-
let insn_id = self.union_find.borrow().find_const(insn_id);
5860-
self.insns[insn_id.0].try_for_each_operand(|operand| {
5861-
let operand = self.union_find.borrow().find_const(operand);
5862-
if !assigned.get(operand) {
5863-
return Err(ValidationError::OperandNotDefined(block, insn_id, operand));
5864-
}
5865-
Ok(())
5866-
})?;
5867-
if self.insns[insn_id.0].has_output() {
5868-
assigned.insert(insn_id);
5869-
}
5870-
}
5871-
}
5872-
Ok(())
5873-
}
5874-
58755848
/// Checks that each instruction('s representative) appears only once in the CFG.
58765849
fn validate_insn_uniqueness(&self) -> Result<(), ValidationError> {
58775850
let mut seen = InsnSet::with_capacity(self.insns.len());
@@ -6189,7 +6162,6 @@ impl Function {
61896162
pub fn validate(&self) -> Result<(), ValidationError> {
61906163
self.validate_block_terminators_and_jumps()?;
61916164
self.validate_definite_assignment()?;
6192-
self.temporary_validate_block_local_definite_assignment()?;
61936165
self.validate_insn_uniqueness()?;
61946166
self.validate_types()?;
61956167
Ok(())
@@ -6756,7 +6728,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
67566728
// Keep compiling blocks until the queue becomes empty
67576729
let mut visited = HashSet::new();
67586730
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
6759-
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() {
67606732
// Compile each block only once
67616733
if visited.contains(&block) { continue; }
67626734
visited.insert(block);
@@ -7927,8 +7899,44 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
79277899
fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledYARVInsn(opcode) });
79287900
break; // End the block
79297901
}
7930-
let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id });
7931-
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+
}
79327940
}
79337941
YARVINSN_setinstancevariable => {
79347942
let id = ID(get_arg(pc, 0).as_u64());
@@ -8761,17 +8769,6 @@ mod validation_tests {
87618769
assert_matches_err(function.validate_definite_assignment(), ValidationError::OperandNotDefined(entry, val, dangling));
87628770
}
87638771

8764-
#[test]
8765-
fn not_defined_within_bb_block_local() {
8766-
let mut function = Function::new(std::ptr::null());
8767-
let entry = function.entry_block;
8768-
// Create an instruction without making it belong to anything.
8769-
let dangling = function.new_insn(Insn::Const{val: Const::CBool(true)});
8770-
let val = function.push_insn(function.entry_block, Insn::ArrayDup { val: dangling, state: InsnId(0usize) });
8771-
function.seal_entries();
8772-
assert_matches_err(function.temporary_validate_block_local_definite_assignment(), ValidationError::OperandNotDefined(entry, val, dangling));
8773-
}
8774-
87758772
#[test]
87768773
fn using_non_output_insn() {
87778774
let mut function = Function::new(std::ptr::null());
@@ -8784,18 +8781,6 @@ mod validation_tests {
87848781
assert_matches_err(function.validate_definite_assignment(), ValidationError::OperandNotDefined(entry, val, ret));
87858782
}
87868783

8787-
#[test]
8788-
fn using_non_output_insn_block_local() {
8789-
let mut function = Function::new(std::ptr::null());
8790-
let entry = function.entry_block;
8791-
let const_ = function.push_insn(function.entry_block, Insn::Const{val: Const::CBool(true)});
8792-
// Ret is a non-output instruction.
8793-
let ret = function.push_insn(function.entry_block, Insn::Return { val: const_ });
8794-
let val = function.push_insn(function.entry_block, Insn::ArrayDup { val: ret, state: InsnId(0usize) });
8795-
function.seal_entries();
8796-
assert_matches_err(function.temporary_validate_block_local_definite_assignment(), ValidationError::OperandNotDefined(entry, val, ret));
8797-
}
8798-
87998784
#[test]
88008785
fn not_dominated_by_diamond() {
88018786
// This tests that one branch is missing a definition which fails.

zjit/src/hir/opt_tests.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7443,7 +7443,68 @@ 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
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+
CheckInterrupts
7502+
Return v13
7503+
");
7504+
}
7505+
7506+
#[test]
7507+
fn test_dont_optimize_attr_accessor_polymorphic() {
74477508
set_call_threshold(3);
74487509
eval("
74497510
class C

0 commit comments

Comments
 (0)