Skip to content

Commit b0754fb

Browse files
authored
ZJIT: Deduplicate polymorphic send branches by receiver type (ruby#16335)
When lowering polymorphic `opt_send_without_block`, emit at most one branch per receiver type instead of one branch per profile bucket (`class, shape, flags`). This deduplicates redundant `HasType`/`IfTrue` chains while preserving immediate/heap splits under the same class (for example, `Fixnum`/`Bignum` and `StaticSymbol`/`DynamicSymbol`), reducing branch/codegen overhead without changing dispatch semantics. Stats diff (`railsbench`): - `code_region_bytes`: `15,171,584` -> `14,974,976` (`-1.30%`) - `side_exit_size`: `4,999,792` -> `4,955,020` (`-0.90%`)
1 parent 1a0e67d commit b0754fb

File tree

2 files changed

+217
-15
lines changed

2 files changed

+217
-15
lines changed

zjit/src/hir.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7670,10 +7670,16 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
76707670
let entry_args = state.as_args(self_param);
76717671
if let Some(summary) = fun.polymorphic_summary(&profiles, recv, exit_state.insn_idx) {
76727672
let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx));
7673-
// TODO(max): Only iterate over unique classes, not unique (class, shape) pairs.
7673+
// Dedup by expected type so immediate/heap variants
7674+
// under the same Ruby class can still get separate branches.
7675+
let mut seen_types = Vec::with_capacity(summary.buckets().len());
76747676
for &profiled_type in summary.buckets() {
76757677
if profiled_type.is_empty() { break; }
76767678
let expected = Type::from_profiled_type(profiled_type);
7679+
if seen_types.iter().any(|ty: &Type| ty.bit_equal(expected)) {
7680+
continue;
7681+
}
7682+
seen_types.push(expected);
76777683
let has_type = fun.push_insn(block, Insn::HasType { val: recv, expected });
76787684
let iftrue_block =
76797685
new_branch_block(&mut fun, cd, argc as usize, opcode, expected, branch_insn_idx, &exit_state, locals_count, stack_count, join_block);

zjit/src/hir/opt_tests.rs

Lines changed: 210 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7406,25 +7406,17 @@ mod hir_opt_tests {
74067406
bb3(v9:BasicObject, v10:BasicObject):
74077407
v15:CBool = HasType v10, ObjectSubclass[class_exact:C]
74087408
IfTrue v15, bb5(v9, v10, v10)
7409-
v24:CBool = HasType v10, ObjectSubclass[class_exact:C]
7410-
IfTrue v24, bb6(v9, v10, v10)
7411-
v33:BasicObject = Send v10, :foo # SendFallbackReason: SendWithoutBlock: polymorphic fallback
7412-
Jump bb4(v9, v10, v33)
7409+
v24:BasicObject = Send v10, :foo # SendFallbackReason: SendWithoutBlock: polymorphic fallback
7410+
Jump bb4(v9, v10, v24)
74137411
bb5(v16:BasicObject, v17:BasicObject, v18:BasicObject):
74147412
v20:ObjectSubclass[class_exact:C] = RefineType v18, ObjectSubclass[class_exact:C]
74157413
PatchPoint NoSingletonClass(C@0x1008)
74167414
PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018)
7417-
v46:BasicObject = GetIvar v20, :@foo
7418-
Jump bb4(v16, v17, v46)
7419-
bb6(v25:BasicObject, v26:BasicObject, v27:BasicObject):
7420-
v29:ObjectSubclass[class_exact:C] = RefineType v27, ObjectSubclass[class_exact:C]
7421-
PatchPoint NoSingletonClass(C@0x1008)
7422-
PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018)
7423-
v49:BasicObject = GetIvar v29, :@foo
7424-
Jump bb4(v25, v26, v49)
7425-
bb4(v35:BasicObject, v36:BasicObject, v37:BasicObject):
7415+
v37:BasicObject = GetIvar v20, :@foo
7416+
Jump bb4(v16, v17, v37)
7417+
bb4(v26:BasicObject, v27:BasicObject, v28:BasicObject):
74267418
CheckInterrupts
7427-
Return v37
7419+
Return v28
74287420
");
74297421
}
74307422

@@ -13834,6 +13826,210 @@ mod hir_opt_tests {
1383413826
");
1383513827
}
1383613828

13829+
#[test]
13830+
fn specialize_polymorphic_send_fixnum_and_bignum() {
13831+
// Fixnum and Bignum both have class Integer, but they should be
13832+
// treated as different types for polymorphic dispatch because
13833+
// Fixnum is an immediate and Bignum is a heap object.
13834+
set_call_threshold(4);
13835+
eval("
13836+
def test x
13837+
x.to_s
13838+
end
13839+
13840+
fixnum = 1
13841+
bignum = 10**100
13842+
test(fixnum)
13843+
test(bignum)
13844+
test(fixnum)
13845+
test(bignum)
13846+
");
13847+
assert_snapshot!(hir_string("test"), @"
13848+
fn test@<compiled>:3:
13849+
bb1():
13850+
EntryPoint interpreter
13851+
v1:BasicObject = LoadSelf
13852+
v2:CPtr = LoadSP
13853+
v3:BasicObject = LoadField v2, :x@0x1000
13854+
Jump bb3(v1, v3)
13855+
bb2():
13856+
EntryPoint JIT(0)
13857+
v6:BasicObject = LoadArg :self@0
13858+
v7:BasicObject = LoadArg :x@1
13859+
Jump bb3(v6, v7)
13860+
bb3(v9:BasicObject, v10:BasicObject):
13861+
v15:CBool = HasType v10, Fixnum
13862+
IfTrue v15, bb5(v9, v10, v10)
13863+
v24:CBool = HasType v10, Bignum
13864+
IfTrue v24, bb6(v9, v10, v10)
13865+
v33:BasicObject = Send v10, :to_s # SendFallbackReason: SendWithoutBlock: polymorphic fallback
13866+
Jump bb4(v9, v10, v33)
13867+
bb5(v16:BasicObject, v17:BasicObject, v18:BasicObject):
13868+
v20:Fixnum = RefineType v18, Fixnum
13869+
PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018)
13870+
v46:StringExact = CCallVariadic v20, :Integer#to_s@0x1040
13871+
Jump bb4(v16, v17, v46)
13872+
bb6(v25:BasicObject, v26:BasicObject, v27:BasicObject):
13873+
v29:Bignum = RefineType v27, Bignum
13874+
PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018)
13875+
v49:StringExact = CCallVariadic v29, :Integer#to_s@0x1040
13876+
Jump bb4(v25, v26, v49)
13877+
bb4(v35:BasicObject, v36:BasicObject, v37:BasicObject):
13878+
CheckInterrupts
13879+
Return v37
13880+
");
13881+
}
13882+
13883+
#[test]
13884+
fn specialize_polymorphic_send_flonum_and_heap_float() {
13885+
set_call_threshold(4);
13886+
eval("
13887+
def test x
13888+
x.to_s
13889+
end
13890+
13891+
flonum = 1.5
13892+
heap_float = 1.7976931348623157e+308
13893+
test(flonum)
13894+
test(heap_float)
13895+
test(flonum)
13896+
test(heap_float)
13897+
");
13898+
assert_snapshot!(hir_string("test"), @"
13899+
fn test@<compiled>:3:
13900+
bb1():
13901+
EntryPoint interpreter
13902+
v1:BasicObject = LoadSelf
13903+
v2:CPtr = LoadSP
13904+
v3:BasicObject = LoadField v2, :x@0x1000
13905+
Jump bb3(v1, v3)
13906+
bb2():
13907+
EntryPoint JIT(0)
13908+
v6:BasicObject = LoadArg :self@0
13909+
v7:BasicObject = LoadArg :x@1
13910+
Jump bb3(v6, v7)
13911+
bb3(v9:BasicObject, v10:BasicObject):
13912+
v15:CBool = HasType v10, Flonum
13913+
IfTrue v15, bb5(v9, v10, v10)
13914+
v24:CBool = HasType v10, HeapFloat
13915+
IfTrue v24, bb6(v9, v10, v10)
13916+
v33:BasicObject = Send v10, :to_s # SendFallbackReason: SendWithoutBlock: polymorphic fallback
13917+
Jump bb4(v9, v10, v33)
13918+
bb5(v16:BasicObject, v17:BasicObject, v18:BasicObject):
13919+
v20:Flonum = RefineType v18, Flonum
13920+
PatchPoint MethodRedefined(Float@0x1008, to_s@0x1010, cme:0x1018)
13921+
v46:BasicObject = CCallWithFrame v20, :Float#to_s@0x1040
13922+
Jump bb4(v16, v17, v46)
13923+
bb6(v25:BasicObject, v26:BasicObject, v27:BasicObject):
13924+
v29:HeapFloat = RefineType v27, HeapFloat
13925+
PatchPoint MethodRedefined(Float@0x1008, to_s@0x1010, cme:0x1018)
13926+
v49:BasicObject = CCallWithFrame v29, :Float#to_s@0x1040
13927+
Jump bb4(v25, v26, v49)
13928+
bb4(v35:BasicObject, v36:BasicObject, v37:BasicObject):
13929+
CheckInterrupts
13930+
Return v37
13931+
");
13932+
}
13933+
13934+
#[test]
13935+
fn specialize_polymorphic_send_static_and_dynamic_symbol() {
13936+
set_call_threshold(4);
13937+
eval("
13938+
def test x
13939+
x.to_s
13940+
end
13941+
13942+
static_sym = :foo
13943+
dynamic_sym = (\"zjit_dynamic_\" + Object.new.object_id.to_s).to_sym
13944+
test static_sym
13945+
test dynamic_sym
13946+
test static_sym
13947+
test dynamic_sym
13948+
");
13949+
assert_snapshot!(hir_string("test"), @"
13950+
fn test@<compiled>:3:
13951+
bb1():
13952+
EntryPoint interpreter
13953+
v1:BasicObject = LoadSelf
13954+
v2:CPtr = LoadSP
13955+
v3:BasicObject = LoadField v2, :x@0x1000
13956+
Jump bb3(v1, v3)
13957+
bb2():
13958+
EntryPoint JIT(0)
13959+
v6:BasicObject = LoadArg :self@0
13960+
v7:BasicObject = LoadArg :x@1
13961+
Jump bb3(v6, v7)
13962+
bb3(v9:BasicObject, v10:BasicObject):
13963+
v15:CBool = HasType v10, StaticSymbol
13964+
IfTrue v15, bb5(v9, v10, v10)
13965+
v24:CBool = HasType v10, DynamicSymbol
13966+
IfTrue v24, bb6(v9, v10, v10)
13967+
v33:BasicObject = Send v10, :to_s # SendFallbackReason: SendWithoutBlock: polymorphic fallback
13968+
Jump bb4(v9, v10, v33)
13969+
bb5(v16:BasicObject, v17:BasicObject, v18:BasicObject):
13970+
v20:StaticSymbol = RefineType v18, StaticSymbol
13971+
PatchPoint MethodRedefined(Symbol@0x1008, to_s@0x1010, cme:0x1018)
13972+
v48:StringExact = InvokeBuiltin leaf <inline_expr>, v20
13973+
Jump bb4(v16, v17, v48)
13974+
bb6(v25:BasicObject, v26:BasicObject, v27:BasicObject):
13975+
v29:DynamicSymbol = RefineType v27, DynamicSymbol
13976+
PatchPoint MethodRedefined(Symbol@0x1008, to_s@0x1010, cme:0x1018)
13977+
v49:StringExact = InvokeBuiltin leaf <inline_expr>, v29
13978+
Jump bb4(v25, v26, v49)
13979+
bb4(v35:BasicObject, v36:BasicObject, v37:BasicObject):
13980+
CheckInterrupts
13981+
Return v37
13982+
");
13983+
}
13984+
13985+
#[test]
13986+
fn specialize_polymorphic_send_iseq_duplicate_class_profiles() {
13987+
set_call_threshold(4);
13988+
eval("
13989+
class C
13990+
def foo = 3
13991+
end
13992+
13993+
O1 = C.new
13994+
O1.instance_variable_set(:@foo, 1)
13995+
O2 = C.new
13996+
O2.instance_variable_set(:@bar, 2)
13997+
13998+
def test o
13999+
o.foo
14000+
end
14001+
14002+
test O1; test O2; test O1; test O2
14003+
");
14004+
assert_snapshot!(hir_string("test"), @"
14005+
fn test@<compiled>:12:
14006+
bb1():
14007+
EntryPoint interpreter
14008+
v1:BasicObject = LoadSelf
14009+
v2:CPtr = LoadSP
14010+
v3:BasicObject = LoadField v2, :o@0x1000
14011+
Jump bb3(v1, v3)
14012+
bb2():
14013+
EntryPoint JIT(0)
14014+
v6:BasicObject = LoadArg :self@0
14015+
v7:BasicObject = LoadArg :o@1
14016+
Jump bb3(v6, v7)
14017+
bb3(v9:BasicObject, v10:BasicObject):
14018+
v15:CBool = HasType v10, ObjectSubclass[class_exact:C]
14019+
IfTrue v15, bb5(v9, v10, v10)
14020+
v24:BasicObject = Send v10, :foo # SendFallbackReason: SendWithoutBlock: polymorphic fallback
14021+
Jump bb4(v9, v10, v24)
14022+
bb5(v16:BasicObject, v17:BasicObject, v18:BasicObject):
14023+
PatchPoint NoSingletonClass(C@0x1008)
14024+
PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018)
14025+
v38:Fixnum[3] = Const Value(3)
14026+
Jump bb4(v16, v17, v38)
14027+
bb4(v26:BasicObject, v27:BasicObject, v28:BasicObject):
14028+
CheckInterrupts
14029+
Return v28
14030+
");
14031+
}
14032+
1383714033
#[test]
1383814034
fn upgrade_self_type_to_heap_after_setivar() {
1383914035
eval("

0 commit comments

Comments
 (0)