Skip to content

Commit bc6c895

Browse files
authored
ZJIT: Create HIR effect system (ruby#15359)
**Progress** I've added a new directory, `zjit/src/hir_effect`. It follows the same structure as `zjit/src/hir_type` and includes: - a ruby script to generate a rust file containing a bitset of effects we want to track - a modified `hir.rs` to include an `effects_of` function that catalogs effects for each HIR instruction, similar to `infer_type`. Right now these effects are not specialized, all instructions currently return the top of the lattice (any effect) - a module file for effects at `zjit/src/hir_effect/mod.rs` that again, mirrors `zjit/src/hir_type/mod.rs`. This contains a lot of helper functions and lattice operations like union and intersection **Design Idea** The effect system is bitset-based rather than range-based. This is the first kind of effect system described in [Max's blog post](https://bernsteinbear.com/blog/compiler-effects/). Practically, having effects defined for each HIR instruction should allow us to have better generalization than the implicit effect system we have for c functions that we annotation as elidable, leaf, etc. Additionally, this could allow us to reason about the effects of multiple HIR instructions unioned together, something I don't believe currently exists. **Practical Goals** This PR replaces `has_effects` with a new effects-based `is_elidable` function. This has no behavior change to the JIT, but will make it easier to reason about effects of basic blocks and CCalls with the new design. We may be able to accomplish other quality of life improvements, such as consolidation of `nogc`, `leaf`, and other annotations.
1 parent 91744cd commit bc6c895

File tree

5 files changed

+758
-57
lines changed

5 files changed

+758
-57
lines changed

zjit/src/hir.rs

Lines changed: 163 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::{
1313
cell::RefCell, collections::{BTreeSet, HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter
1414
};
1515
use crate::hir_type::{Type, types};
16+
use crate::hir_effect::{Effect, abstract_heaps, effects};
1617
use crate::bitset::BitSet;
1718
use crate::profile::{TypeDistributionSummary, ProfiledType};
1819
use crate::stats::Counter;
@@ -1053,61 +1054,167 @@ impl Insn {
10531054
InsnPrinter { inner: self.clone(), ptr_map, iseq }
10541055
}
10551056

1056-
/// Return true if the instruction needs to be kept around. For example, if the instruction
1057-
/// might have a side effect, or if the instruction may raise an exception.
1058-
fn has_effects(&self) -> bool {
1059-
match self {
1060-
Insn::Const { .. } => false,
1061-
Insn::Param => false,
1062-
Insn::StringCopy { .. } => false,
1063-
Insn::NewArray { .. } => false,
1064-
// NewHash's operands may be hashed and compared for equality, which could have
1065-
// side-effects.
1066-
Insn::NewHash { elements, .. } => !elements.is_empty(),
1067-
Insn::ArrayLength { .. } => false,
1068-
Insn::ArrayDup { .. } => false,
1069-
Insn::HashDup { .. } => false,
1070-
Insn::Test { .. } => false,
1071-
Insn::Snapshot { .. } => false,
1072-
Insn::FixnumAdd { .. } => false,
1073-
Insn::FixnumSub { .. } => false,
1074-
Insn::FixnumMult { .. } => false,
1075-
// TODO(max): Consider adding a Guard that the rhs is non-zero before Div and Mod
1076-
// Div *is* critical unless we can prove the right hand side != 0
1077-
// Mod *is* critical unless we can prove the right hand side != 0
1078-
Insn::FixnumEq { .. } => false,
1079-
Insn::FixnumNeq { .. } => false,
1080-
Insn::FixnumLt { .. } => false,
1081-
Insn::FixnumLe { .. } => false,
1082-
Insn::FixnumGt { .. } => false,
1083-
Insn::FixnumGe { .. } => false,
1084-
Insn::FixnumAnd { .. } => false,
1085-
Insn::FixnumOr { .. } => false,
1086-
Insn::FixnumXor { .. } => false,
1087-
Insn::FixnumLShift { .. } => false,
1088-
Insn::FixnumRShift { .. } => false,
1089-
Insn::FixnumAref { .. } => false,
1090-
Insn::GetLocal { .. } => false,
1091-
Insn::IsNil { .. } => false,
1092-
Insn::LoadPC => false,
1093-
Insn::LoadEC => false,
1094-
Insn::LoadSelf => false,
1095-
Insn::LoadField { .. } => false,
1096-
Insn::CCall { elidable, .. } => !elidable,
1097-
Insn::CCallWithFrame { elidable, .. } => !elidable,
1098-
Insn::ObjectAllocClass { .. } => false,
1099-
// TODO: NewRange is effects free if we can prove the two ends to be Fixnum,
1100-
// but we don't have type information here in `impl Insn`. See rb_range_new().
1101-
Insn::NewRange { .. } => true,
1102-
Insn::NewRangeFixnum { .. } => false,
1103-
Insn::StringGetbyte { .. } => false,
1104-
Insn::IsBlockGiven => false,
1105-
Insn::BoxFixnum { .. } => false,
1106-
Insn::BoxBool { .. } => false,
1107-
Insn::IsBitEqual { .. } => false,
1108-
Insn::IsA { .. } => false,
1109-
_ => true,
1110-
}
1057+
// TODO(Jacob): Model SP. ie, all allocations modify stack size but using the effect for stack modification feels excessive
1058+
// TODO(Jacob): Add sideeffect failure bit
1059+
fn effects_of(&self) -> Effect {
1060+
const allocates: Effect = Effect::read_write(abstract_heaps::PC.union(abstract_heaps::Allocator), abstract_heaps::Allocator);
1061+
match &self {
1062+
Insn::Const { .. } => effects::Empty,
1063+
Insn::Param { .. } => effects::Empty,
1064+
Insn::StringCopy { .. } => allocates,
1065+
Insn::StringIntern { .. } => effects::Any,
1066+
Insn::StringConcat { .. } => effects::Any,
1067+
Insn::StringGetbyte { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty),
1068+
Insn::StringSetbyteFixnum { .. } => effects::Any,
1069+
Insn::StringAppend { .. } => effects::Any,
1070+
Insn::StringAppendCodepoint { .. } => effects::Any,
1071+
Insn::ToRegexp { .. } => effects::Any,
1072+
Insn::PutSpecialObject { .. } => effects::Any,
1073+
Insn::ToArray { .. } => effects::Any,
1074+
Insn::ToNewArray { .. } => effects::Any,
1075+
Insn::NewArray { .. } => allocates,
1076+
Insn::NewHash { elements, .. } => {
1077+
// NewHash's operands may be hashed and compared for equality, which could have
1078+
// side-effects. Empty hashes are definitely elidable.
1079+
if elements.is_empty() {
1080+
Effect::write(abstract_heaps::Allocator)
1081+
}
1082+
else {
1083+
effects::Any
1084+
}
1085+
},
1086+
Insn::NewRange { .. } => effects::Any,
1087+
Insn::NewRangeFixnum { .. } => allocates,
1088+
Insn::ArrayDup { .. } => allocates,
1089+
Insn::ArrayHash { .. } => effects::Any,
1090+
Insn::ArrayMax { .. } => effects::Any,
1091+
Insn::ArrayInclude { .. } => effects::Any,
1092+
Insn::ArrayPackBuffer { .. } => effects::Any,
1093+
Insn::DupArrayInclude { .. } => effects::Any,
1094+
Insn::ArrayExtend { .. } => effects::Any,
1095+
Insn::ArrayPush { .. } => effects::Any,
1096+
Insn::ArrayAref { .. } => effects::Any,
1097+
Insn::ArrayAset { .. } => effects::Any,
1098+
Insn::ArrayPop { .. } => effects::Any,
1099+
Insn::ArrayLength { .. } => Effect::write(abstract_heaps::Empty),
1100+
Insn::HashAref { .. } => effects::Any,
1101+
Insn::HashAset { .. } => effects::Any,
1102+
Insn::HashDup { .. } => allocates,
1103+
Insn::ObjectAlloc { .. } => effects::Any,
1104+
Insn::ObjectAllocClass { .. } => allocates,
1105+
Insn::Test { .. } => effects::Empty,
1106+
Insn::IsNil { .. } => effects::Empty,
1107+
Insn::IsMethodCfunc { .. } => effects::Any,
1108+
Insn::IsBitEqual { .. } => effects::Empty,
1109+
Insn::IsBitNotEqual { .. } => effects::Any,
1110+
Insn::BoxBool { .. } => effects::Empty,
1111+
Insn::BoxFixnum { .. } => effects::Empty,
1112+
Insn::UnboxFixnum { .. } => effects::Any,
1113+
Insn::FixnumAref { .. } => effects::Empty,
1114+
Insn::Defined { .. } => effects::Any,
1115+
Insn::GetConstantPath { .. } => effects::Any,
1116+
Insn::IsBlockGiven { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty),
1117+
Insn::FixnumBitCheck { .. } => effects::Any,
1118+
Insn::IsA { .. } => effects::Empty,
1119+
Insn::GetGlobal { .. } => effects::Any,
1120+
Insn::SetGlobal { .. } => effects::Any,
1121+
Insn::GetIvar { .. } => effects::Any,
1122+
Insn::SetIvar { .. } => effects::Any,
1123+
Insn::DefinedIvar { .. } => effects::Any,
1124+
Insn::LoadPC { .. } => Effect::read_write(abstract_heaps::PC, abstract_heaps::Empty),
1125+
Insn::LoadEC { .. } => effects::Empty,
1126+
Insn::LoadSelf { .. } => Effect::read_write(abstract_heaps::Frame, abstract_heaps::Empty),
1127+
Insn::LoadField { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty),
1128+
Insn::StoreField { .. } => effects::Any,
1129+
Insn::WriteBarrier { .. } => effects::Any,
1130+
Insn::GetLocal { .. } => Effect::read_write(abstract_heaps::Locals, abstract_heaps::Empty),
1131+
Insn::SetLocal { .. } => effects::Any,
1132+
Insn::GetSpecialSymbol { .. } => effects::Any,
1133+
Insn::GetSpecialNumber { .. } => effects::Any,
1134+
Insn::GetClassVar { .. } => effects::Any,
1135+
Insn::SetClassVar { .. } => effects::Any,
1136+
Insn::Snapshot { .. } => effects::Empty,
1137+
Insn::Jump(_) => effects::Any,
1138+
Insn::IfTrue { .. } => effects::Any,
1139+
Insn::IfFalse { .. } => effects::Any,
1140+
Insn::CCall { elidable, .. } => {
1141+
if *elidable {
1142+
Effect::write(abstract_heaps::Allocator)
1143+
}
1144+
else {
1145+
effects::Any
1146+
}
1147+
},
1148+
Insn::CCallWithFrame { elidable, .. } => {
1149+
if *elidable {
1150+
Effect::write(abstract_heaps::Allocator)
1151+
}
1152+
else {
1153+
effects::Any
1154+
}
1155+
},
1156+
Insn::CCallVariadic { .. } => effects::Any,
1157+
Insn::SendWithoutBlock { .. } => effects::Any,
1158+
Insn::Send { .. } => effects::Any,
1159+
Insn::SendForward { .. } => effects::Any,
1160+
Insn::InvokeSuper { .. } => effects::Any,
1161+
Insn::InvokeBlock { .. } => effects::Any,
1162+
Insn::SendWithoutBlockDirect { .. } => effects::Any,
1163+
Insn::InvokeBuiltin { .. } => effects::Any,
1164+
Insn::EntryPoint { .. } => effects::Any,
1165+
Insn::Return { .. } => effects::Any,
1166+
Insn::Throw { .. } => effects::Any,
1167+
Insn::FixnumAdd { .. } => effects::Empty,
1168+
Insn::FixnumSub { .. } => effects::Empty,
1169+
Insn::FixnumMult { .. } => effects::Empty,
1170+
Insn::FixnumDiv { .. } => effects::Any,
1171+
Insn::FixnumMod { .. } => effects::Any,
1172+
Insn::FixnumEq { .. } => effects::Empty,
1173+
Insn::FixnumNeq { .. } => effects::Empty,
1174+
Insn::FixnumLt { .. } => effects::Empty,
1175+
Insn::FixnumLe { .. } => effects::Empty,
1176+
Insn::FixnumGt { .. } => effects::Empty,
1177+
Insn::FixnumGe { .. } => effects::Empty,
1178+
Insn::FixnumAnd { .. } => effects::Empty,
1179+
Insn::FixnumOr { .. } => effects::Empty,
1180+
Insn::FixnumXor { .. } => effects::Empty,
1181+
Insn::FixnumLShift { .. } => effects::Empty,
1182+
Insn::FixnumRShift { .. } => effects::Empty,
1183+
Insn::ObjToString { .. } => effects::Any,
1184+
Insn::AnyToString { .. } => effects::Any,
1185+
Insn::GuardType { .. } => effects::Any,
1186+
Insn::GuardTypeNot { .. } => effects::Any,
1187+
Insn::GuardBitEquals { .. } => effects::Any,
1188+
Insn::GuardShape { .. } => effects::Any,
1189+
Insn::GuardBlockParamProxy { .. } => effects::Any,
1190+
Insn::GuardNotFrozen { .. } => effects::Any,
1191+
Insn::GuardNotShared { .. } => effects::Any,
1192+
Insn::GuardGreaterEq { .. } => effects::Any,
1193+
Insn::GuardSuperMethodEntry { .. } => effects::Any,
1194+
Insn::GetBlockHandler { .. } => effects::Any,
1195+
Insn::GuardLess { .. } => effects::Any,
1196+
Insn::PatchPoint { .. } => effects::Any,
1197+
Insn::SideExit { .. } => effects::Any,
1198+
Insn::IncrCounter(_) => effects::Any,
1199+
Insn::IncrCounterPtr { .. } => effects::Any,
1200+
Insn::CheckInterrupts { .. } => effects::Any,
1201+
}
1202+
}
1203+
1204+
/// Return true if we can safely omit the instruction. This occurs when one of the following
1205+
/// conditions are met.
1206+
/// 1. The instruction does not write anything.
1207+
/// 2. The instruction only allocates and writes nothing else.
1208+
/// Calling the effects of our instruction `insn_effects`, we need:
1209+
/// `effects::Empty` to include `insn_effects.write` or `effects::Allocator` to include
1210+
/// `insn_effects.write`.
1211+
/// We can simplify this to `effects::Empty.union(effects::Allocator).includes(insn_effects.write)`.
1212+
/// But the union of `Allocator` and `Empty` is simply `Allocator`, so our entire function
1213+
/// collapses to `effects::Allocator.includes(insn_effects.write)`.
1214+
/// Note: These are restrictions on the `write` `EffectSet` only. Even instructions with
1215+
/// `read: effects::Any` could potentially be omitted.
1216+
fn is_elidable(&self) -> bool {
1217+
abstract_heaps::Allocator.includes(self.effects_of().write_bits())
11111218
}
11121219
}
11131220

@@ -4388,8 +4495,7 @@ impl Function {
43884495
// otherwise necessary to keep around
43894496
for block_id in &rpo {
43904497
for insn_id in &self.blocks[block_id.0].insns {
4391-
let insn = &self.insns[insn_id.0];
4392-
if insn.has_effects() {
4498+
if !&self.insns[insn_id.0].is_elidable() {
43934499
worklist.push_back(*insn_id);
43944500
}
43954501
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Generate hir_effect.inc.rs. To do this, we build up a DAG that
2+
# represents the ZJIT effect hierarchy.
3+
4+
require 'set'
5+
6+
# Effect represents not just a Ruby class but a named union of other effects.
7+
class Effect
8+
attr_accessor :name, :subeffects
9+
10+
def initialize name, subeffects=nil
11+
@name = name
12+
@subeffects = subeffects || []
13+
end
14+
15+
def all_subeffects
16+
subeffects.flat_map { |subeffect| subeffect.all_subeffects } + subeffects
17+
end
18+
19+
def subeffect name
20+
result = Effect.new name
21+
@subeffects << result
22+
result
23+
end
24+
end
25+
26+
# Helper to generate graphviz.
27+
def to_graphviz_rec effect
28+
effect.subeffects.each {|subeffect|
29+
puts effect.name + "->" + subeffect.name + ";"
30+
}
31+
effect.subeffect.each {|subeffect|
32+
to_graphviz_rec subeffect
33+
}
34+
end
35+
36+
# Generate graphviz.
37+
def to_graphviz effect
38+
puts "digraph G {"
39+
to_graphviz_rec effect
40+
puts "}"
41+
end
42+
43+
# ===== Start generating the effect DAG =====
44+
45+
# Start at Any. All effects are subeffects of Any.
46+
any = Effect.new 'Any'
47+
# Build the effect universe.
48+
allocator = any.subeffect 'Allocator'
49+
control = any.subeffect 'Control'
50+
memory = any.subeffect 'Memory'
51+
other = memory.subeffect 'Other'
52+
frame = memory.subeffect 'Frame'
53+
pc = frame.subeffect 'PC'
54+
locals = frame.subeffect 'Locals'
55+
stack = frame.subeffect 'Stack'
56+
57+
# Use the smallest unsigned value needed to describe all effect bits
58+
# If it becomes an issue, this can be generated but for now we do it manually
59+
$int_label = 'u8'
60+
61+
# Assign individual bits to effect leaves and union bit patterns to nodes with subeffects
62+
num_bits = 0
63+
$bits = {"Empty" => ["0#{$int_label}"]}
64+
$numeric_bits = {"Empty" => 0}
65+
Set[any, *any.all_subeffects].sort_by(&:name).each {|effect|
66+
subeffects = effect.subeffects
67+
if subeffects.empty?
68+
# Assign bits for leaves
69+
$bits[effect.name] = ["1#{$int_label} << #{num_bits}"]
70+
$numeric_bits[effect.name] = 1 << num_bits
71+
num_bits += 1
72+
else
73+
# Assign bits for unions
74+
$bits[effect.name] = subeffects.map(&:name).sort
75+
end
76+
}
77+
[*any.all_subeffects, any].each {|effect|
78+
subeffects = effect.subeffects
79+
unless subeffects.empty?
80+
$numeric_bits[effect.name] = subeffects.map {|ty| $numeric_bits[ty.name]}.reduce(&:|)
81+
end
82+
}
83+
84+
# ===== Finished generating the DAG; write Rust code =====
85+
86+
puts "// This file is @generated by src/hir/gen_hir_effect.rb."
87+
puts "mod bits {"
88+
$bits.keys.sort.map {|effect_name|
89+
subeffects = $bits[effect_name].join(" | ")
90+
puts " pub const #{effect_name}: #{$int_label} = #{subeffects};"
91+
}
92+
puts " pub const AllBitPatterns: [(&str, #{$int_label}); #{$bits.size}] = ["
93+
# Sort the bit patterns by decreasing value so that we can print the densest
94+
# possible to-string representation of an Effect. For example, Frame instead of
95+
# PC|Stack|Locals
96+
$numeric_bits.sort_by {|key, val| -val}.each {|effect_name, _|
97+
puts " (\"#{effect_name}\", #{effect_name}),"
98+
}
99+
puts " ];"
100+
puts " pub const NumEffectBits: #{$int_label} = #{num_bits};
101+
}"
102+
103+
puts "pub mod effect_types {"
104+
puts " pub type EffectBits = #{$int_label};"
105+
puts "}"
106+
107+
puts "pub mod abstract_heaps {
108+
use super::*;"
109+
$bits.keys.sort.map {|effect_name|
110+
puts " pub const #{effect_name}: AbstractHeap = AbstractHeap::from_bits(bits::#{effect_name});"
111+
}
112+
puts "}"
113+
114+
puts "pub mod effects {
115+
use super::*;"
116+
$bits.keys.sort.map {|effect_name|
117+
puts " pub const #{effect_name}: Effect = Effect::promote(abstract_heaps::#{effect_name});"
118+
}
119+
puts "}"

0 commit comments

Comments
 (0)