Skip to content

Commit 3d34e14

Browse files
committed
Improve documentation and readability
1 parent f65182f commit 3d34e14

2 files changed

Lines changed: 71 additions & 89 deletions

File tree

zjit/src/hir.rs

Lines changed: 69 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5851,28 +5851,33 @@ impl Function {
58515851
}
58525852
}
58535853

5854-
5855-
// TODO: Add a comment about the paper and where this function comes from, as well as how it's used in ZJIT
5856-
// (It's used more individually and differently than the paper)
5857-
// TODO: Update comments to consider block params rather than phi nodes and demarcate differences from the algorithm clearly
5858-
// TODO: Fix input arguments. We need block params, not just the phi
5859-
// If all possible phi values are the same, replace the phi with the value
5860-
// TODO: Add Max's optimization about not checking the first block. Maybe do this by keeping track of all changing edges and using a worklist?
5861-
5862-
/// Sometimes block params can only come from one place and safely removed as block params.
5863-
/// Trivial block param removal increases the efficacy of CFG-based optimization passes.
5864-
/// This function implements algorithm 2 from <https://c9x.me/compile/bib/braun13cc.pdf>.
5865-
/// Light modifications are made to use block params instead of phis.
5854+
/// ZJIT uses block parameters in HIR SSA representation.
5855+
/// Sometimes, we can prove that a block param is only called with a single value.
5856+
/// This pass identifies such trivial block params and replaces them with the concretized value.
5857+
/// This produces a minimal SSA representation amenable to further optimizations.
5858+
/// The implementation is inspired from algorithm 2 in <https://c9x.me/compile/bib/braun13cc.pdf>.
58665859
fn remove_trivial_block_params(&mut self) {
5867-
// Each block param corresponds to a ParamValue
5868-
// This is the abstract domain used for abstract interpretation
5869-
// If there are no predecessors or multiple predecessors to a param, no optimization can happen
5870-
// However if there is a single unique predecessor, then the block param is trivial and we can replace with its concretized value
5871-
#[derive(Clone)]
5860+
// Each block param is lifted to an abstract domain of ParamValues.
5861+
// The lattice is simple. None is Bottom, Multiple is Top, and One is between both.
5862+
// During analysis, all block params start with None.
5863+
// New values passed to the block transition up the lattice.
5864+
// Trivial block params have one unique value. This is the case we optimize away.
5865+
// Lattice structure taken from cranelift: <https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/codegen/src/remove_constant_phis.rs>
5866+
#[derive(Clone, Copy)]
58725867
enum ParamValue {
58735868
None,
58745869
One(InsnId),
5875-
Multiple
5870+
Many
5871+
}
5872+
5873+
impl ParamValue {
5874+
fn update(&mut self, value: InsnId) {
5875+
*self = match *self {
5876+
ParamValue::None => ParamValue::One(value),
5877+
ParamValue::One(original) if original != value => ParamValue::Many,
5878+
other => other
5879+
};
5880+
}
58765881
}
58775882

58785883
// Helper function to remove selected indices from a vec in place
@@ -5892,61 +5897,59 @@ impl Function {
58925897
BranchEdge { target: edge.target, args }
58935898
}
58945899

5895-
fn insn_passes_params(insn: Insn) -> bool {
5896-
match insn {
5897-
Insn::CondBranch {if_true, if_false, ..} => !if_true.args.is_empty() || !if_false.args.is_empty(),
5898-
Insn::Jump(edge) => !edge.args.is_empty(),
5899-
_ => false
5900-
}
5901-
}
5902-
5903-
// Instantiate the domain for abstract interpretation
5904-
// Outer index is block
5905-
// Inner index is param index
5906-
// Value is the state of the param. This is used to determine whether block param optimization can occur.
5900+
// Instantiate the domain for abstract interpretation.
5901+
// We store possible param values for each block
59075902
let mut predecessor_domain: Vec<Vec<ParamValue>> = vec![Vec::new(); self.blocks.len()];
59085903

5909-
let mut updated = true;
5904+
let blocks = self.reverse_post_order();
59105905

59115906
// Find blocks that terminate with Jump or CondBranch instructions that pass block params along.
59125907
// These terminators are later analyzed for trivial block params.
5913-
let param_passing_blocks: Vec<BlockId> = self.reverse_post_order().into_iter()
5908+
let predecessor_blocks: Vec<BlockId> = blocks.iter().copied()
5909+
.filter(|&block_id|
5910+
// Match against the final instruction which terminates the basic block.
5911+
// If it has any non-empty edges, keep it.
5912+
match self.find(*self.blocks[block_id.0].insns().last().unwrap()) {
5913+
Insn::CondBranch {if_true, if_false, ..} => !if_true.args.is_empty() || !if_false.args.is_empty(),
5914+
Insn::Jump(edge) => !edge.args.is_empty(),
5915+
_ => false
5916+
})
5917+
.collect();
5918+
5919+
// We only need to update blocks that have params. (Blocks without params cannot be improved)
5920+
let param_blocks: Vec<BlockId> = blocks.into_iter()
59145921
.filter(|&block_id|
5915-
insn_passes_params(self.find(*self.blocks[block_id.0].insns.last().unwrap())))
5922+
self.blocks[block_id.0].params().len() != 0)
59165923
.collect();
59175924

5918-
while updated {
5925+
// NOTE: It is possible that once some block_params are removed, there will be no params.
5926+
// This means that predecessor_blocks or param_blocks could be pruned. This minor optimization can be added if desired.
5927+
// Importantly, we do not keep track of exactly which edges correspond to which blocks. While doing so
5928+
// would allow us to replace our "loop until fixpoint" with a "iterate through the worklist, only checking relevant edges",
5929+
// the construction of the mapping from predecessor edges to blocks seems expensive.
5930+
5931+
let mut changed = true;
5932+
5933+
while changed {
5934+
changed = false;
59195935

59205936
for (row, block) in predecessor_domain.iter_mut().zip(&self.blocks) {
59215937
row.resize(block.params.len(), ParamValue::None);
59225938
}
5923-
updated = false;
59245939

5925-
// TODO: Maybe move this outside the loop somehow? probably can't immediately, but we could keep track of a worklist of edges that change maybe?
5926-
// And only use the changed ones like a worklist? And then instead of looping to fixpoint we use a worklist based approach?
5927-
//
59285940
// Scan through each jump, collecting edges with params to analyze from CondBranch and Jump insns.
5929-
for block_id in &param_passing_blocks {
5930-
let insn_index = self.blocks[block_id.0].insns.len() - 1;
5931-
let insn_id = self.blocks[block_id.0].insns[insn_index];
5932-
let mut edges: Vec<BranchEdge> = vec![];
5941+
for block_id in &predecessor_blocks {
5942+
let insn_id = *self.blocks[block_id.0].insns.last().unwrap();
5943+
5944+
// Extract edges into a tuple for processing
5945+
let (first, second) = match self.find(insn_id) {
5946+
Insn::Jump(edge) => (Some(edge), None),
5947+
Insn::CondBranch { if_true, if_false, ..} => (Some(if_true), Some(if_false)),
5948+
_ => (None, None)
5949+
};
59335950

5934-
match self.find(insn_id) {
5935-
Insn::CondBranch { if_true, if_false, .. } => {
5936-
if if_true.args.len() > 0 {
5937-
edges.push(if_true);
5938-
}
5939-
if if_false.args.len() > 0 {
5940-
edges.push(if_false);
5941-
}
5942-
}
5943-
Insn::Jump(edge) => {
5944-
if edge.args.len() > 0 {
5945-
edges.push(edge);
5946-
}
5947-
}
5948-
_ => {}
5949-
}
5951+
// Keep all edges that pass params
5952+
let edges = first.into_iter().chain(second).filter(|edge| edge.args.len() > 0);
59505953

59515954
// Use the results of abstract interpretation to update the states
59525955
// Perform abstract interpretation
@@ -5957,18 +5960,7 @@ impl Function {
59575960
if param == self.find_id(self.blocks[block_id.0].params[i]) {
59585961
continue
59595962
}
5960-
let state = &mut predecessor_domain[block_id.0][i];
5961-
match *state {
5962-
ParamValue::None => {
5963-
*state = ParamValue::One(param);
5964-
},
5965-
ParamValue::One(value) => {
5966-
if value != param {
5967-
*state = ParamValue::Multiple;
5968-
}
5969-
}
5970-
ParamValue::Multiple => {},
5971-
}
5963+
predecessor_domain[block_id.0][i].update(param);
59725964
}
59735965
}
59745966
}
@@ -5978,18 +5970,8 @@ impl Function {
59785970
// 1. Replace uses of the trivial params with the concretized value
59795971
// 2. Remove trivial params from the basic block definition
59805972
// 3. Remove trivial params from each CondBranch and Jump that targets the basic block that was just updated
5981-
for (block_id, block_preds) in predecessor_domain.iter().enumerate() {
5982-
// If there are no block params, there is nothing to optimize
5983-
// TODO: We scan predecessors and only keep track of blocks that pass params.
5984-
// We don't do this for block_preds but we should. This requires it kind of becoming hash-mappy again :|
5985-
// This conditional is a stop-gap to get most of the gains from such an optimization, though it should be removed
5986-
// Maybe we can get around this easier by not iterating over the predecessor domain, but over a subset of indices we care about
5987-
if block_preds.len() == 0 {
5988-
continue
5989-
}
5990-
5991-
let block_id = BlockId(block_id);
5992-
5973+
for block_id in &param_blocks {
5974+
let block_preds = &predecessor_domain[block_id.0];
59935975
let trivial_indices: Vec<usize> = block_preds.iter().enumerate()
59945976
.filter_map(|(idx, state)|
59955977
matches!(state, ParamValue::One(_)).then_some(idx)
@@ -5999,31 +5981,31 @@ impl Function {
59995981
for param_index in &trivial_indices {
60005982
if let ParamValue::One(insn_id) = block_preds[*param_index] {
60015983
self.make_equal_to(self.blocks[block_id.0].params[*param_index], insn_id);
6002-
updated = true;
5984+
changed = true;
60035985
}
60045986
}
60055987

60065988
// Update the block
60075989
prune_vec_by_indices(&mut self.blocks[block_id.0].params, &trivial_indices);
60085990

60095991
// Update the terminators (basic blocks can only branch at the terminator. This is where block params are passed)
6010-
for jump_block_id in &param_passing_blocks {
5992+
for jump_block_id in &predecessor_blocks {
60115993
let index = self.blocks[jump_block_id.0].insns.len() - 1;
60125994
let cond_insn_id = self.blocks[jump_block_id.0].insns[index];
60135995
match self.find(cond_insn_id) {
60145996
Insn::Jump(edge) => {
6015-
if edge.target == block_id {
5997+
if edge.target == *block_id {
60165998
let edge = prune_branch_edge(edge, &trivial_indices);
60175999
self.insns[cond_insn_id.0] = Insn::Jump(edge);
60186000
}
60196001
}
60206002
Insn::CondBranch { val, if_true, if_false } => {
6021-
let if_true = if if_true.target == block_id {
6003+
let if_true = if if_true.target == *block_id {
60226004
prune_branch_edge(if_true, &trivial_indices)
60236005
} else {
60246006
if_true
60256007
};
6026-
let if_false = if if_false.target == block_id {
6008+
let if_false = if if_false.target == *block_id {
60276009
prune_branch_edge(if_false, &trivial_indices)
60286010
} else {
60296011
if_false

zjit/src/hir/opt_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21197,15 +21197,15 @@ mod hir_opt_tests {
2119721197
v75:Fixnum = GuardType v37, Fixnum recompile
2119821198
v77:BoolExact = FixnumLe v75, v72
2119921199
v49:CBool = Test v77
21200-
CondBranch v49, bb5(), bb4(v11)
21200+
CondBranch v49, bb5(), bb4()
2120121201
bb5():
2120221202
PatchPoint NoSingletonClass(C@0x1070)
2120321203
PatchPoint MethodRedefined(C@0x1070, foo@0x1078, cme:0x1080)
2120421204
v80:ObjectSubclass[class_exact:C] = GuardType v11, ObjectSubclass[class_exact:C] recompile
2120521205
v81:Fixnum[4] = Const Value(4)
2120621206
CheckInterrupts
2120721207
Return v81
21208-
bb4(v60:HeapBasicObject):
21208+
bb4():
2120921209
v63:NilClass = Const Value(nil)
2121021210
CheckInterrupts
2121121211
Return v63

0 commit comments

Comments
 (0)