Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ssa): Basic control dependent LICM #7660

Draft
wants to merge 31 commits into
base: mv/post-dominator-tree
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b550828
control dependent LICM, only checking whether there are any condition…
vezenovm Mar 11, 2025
f386653
Merge branch 'mv/post-dominator-tree' into mv/control-dep-licm
vezenovm Mar 11, 2025
0d8a0b2
revert test change
vezenovm Mar 11, 2025
38e12f2
Merge remote-tracking branch 'origin/mv/control-dep-licm' into mv/con…
vezenovm Mar 11, 2025
5def442
fixup do_not_hoist_unsafe_div test
vezenovm Mar 11, 2025
2ec7af9
block checking licm control dependence in acir
vezenovm Mar 11, 2025
aa22e24
Merge branch 'mv/post-dominator-tree' into mv/control-dep-licm
vezenovm Mar 11, 2025
3b657b2
store map of predecessors which have been marked control dependent as…
vezenovm Mar 11, 2025
e6e7df6
Merge remote-tracking branch 'origin/mv/control-dep-licm' into mv/con…
vezenovm Mar 11, 2025
2c03c57
Merge branch 'mv/post-dominator-tree' into mv/control-dep-licm
vezenovm Mar 11, 2025
86a391b
still block control dep checks on acir and add comments
vezenovm Mar 11, 2025
73e84eb
move where current_block_control_dependent is set to false
vezenovm Mar 11, 2025
f67e288
current_block_control_dependent always true for acir
vezenovm Mar 11, 2025
e084871
ugh bumping noir_bigcurve timeout
vezenovm Mar 11, 2025
60a01ba
more
vezenovm Mar 11, 2025
57f2864
Merge branch 'mv/post-dominator-tree' into mv/control-dep-licm
vezenovm Mar 11, 2025
536e1a5
rev order we check blocks between preheader and block that may be con…
vezenovm Mar 12, 2025
ae69d80
try to reduce bigcurve timeout
vezenovm Mar 12, 2025
bbee154
Merge branch 'mv/post-dominator-tree' into mv/control-dep-licm
vezenovm Mar 12, 2025
4e9a828
use reverse dom frontiers to move from n^2 control dep check to n
vezenovm Mar 12, 2025
870b669
clippy
vezenovm Mar 12, 2025
1bc58e8
just check dom frontiers and not control dep blocks map
vezenovm Mar 12, 2025
571fa37
one more clippy
vezenovm Mar 12, 2025
5784c62
big curve timeout back to 250
vezenovm Mar 12, 2025
c4e7937
expand instructions that can be hoisted with same predicate in licm
vezenovm Mar 12, 2025
90e9a99
fix licm unit tests
vezenovm Mar 12, 2025
fbebe85
bump timeout
vezenovm Mar 12, 2025
14c8632
dom ffrontier tests
vezenovm Mar 12, 2025
b33626f
improve comments for dom frontiers
vezenovm Mar 12, 2025
096dcb3
add doc comments regarding licm control dep and start checking contro…
vezenovm Mar 12, 2025
61e5f0e
fixup dom frontiers
vezenovm Mar 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions compiler/noirc_evaluator/src/ssa/ir/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ impl ControlFlowGraph {
/// Add a directed edge making `from` a predecessor of `to`.
fn add_edge(&mut self, from: BasicBlockId, to: BasicBlockId) {
let predecessor_node = self.data.entry(from).or_default();
assert!(
predecessor_node.successors.len() < 2,
"ICE: A cfg node cannot have more than two successors"
);
predecessor_node.successors.insert(to);
let successor_node = self.data.entry(to).or_default();
successor_node.predecessors.insert(from);
Expand Down Expand Up @@ -125,7 +121,6 @@ impl ControlFlowGraph {
}

/// Reverse the control flow graph
#[cfg(test)]
pub(crate) fn reverse(&self) -> Self {
let mut reversed_cfg = ControlFlowGraph::default();

Expand Down
8 changes: 4 additions & 4 deletions compiler/noirc_evaluator/src/ssa/ir/dom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,10 @@ impl DominatorTree {
/// "Simple, Fast Dominator Algorithm."
fn compute_dominator_tree(&mut self, cfg: &ControlFlowGraph, post_order: &PostOrder) {
// We'll be iterating over a reverse post-order of the CFG, skipping the entry block.
let (entry_block_id, entry_free_post_order) = post_order
.as_slice()
.split_last()
.expect("ICE: functions always have at least one block");
let Some((entry_block_id, entry_free_post_order)) = post_order.as_slice().split_last()
else {
return;
};

// Do a first pass where we assign reverse post-order indices to all reachable nodes. The
// entry block will be the only node with no immediate dominator.
Expand Down
137 changes: 136 additions & 1 deletion compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crate::ssa::{
Ssa,
ir::{
basic_block::BasicBlockId,
cfg::ControlFlowGraph,
dom::DominatorTree,
function::Function,
function_inserter::FunctionInserter,
instruction::{
Expand Down Expand Up @@ -103,17 +105,30 @@ struct LoopInvariantContext<'f> {
// This stores the current loop's pre-header block.
// It is wrapped in an Option as our SSA `Id<T>` does not allow dummy values.
current_pre_header: Option<BasicBlockId>,

post_dom: DominatorTree,

cfg: ControlFlowGraph,

current_block_control_dependent: bool,
}

impl<'f> LoopInvariantContext<'f> {
fn new(function: &'f mut Function) -> Self {
let cfg = ControlFlowGraph::with_function(function);
let reversed_cfg = cfg.reverse();
let post_order = PostOrder::with_cfg(&reversed_cfg);
let post_dom = DominatorTree::with_cfg_and_post_order(&reversed_cfg, &post_order);
Self {
inserter: FunctionInserter::new(function),
defined_in_loop: HashSet::default(),
loop_invariants: HashSet::default(),
current_induction_variables: HashMap::default(),
outer_induction_variables: HashMap::default(),
current_pre_header: None,
post_dom,
cfg,
current_block_control_dependent: false,
}
}

Expand All @@ -125,6 +140,23 @@ impl<'f> LoopInvariantContext<'f> {
self.set_values_defined_in_loop(loop_);

for block in loop_.blocks.iter() {
let mut all_predecessors =
Loop::find_blocks_in_loop(self.pre_header(), *block, &self.cfg).blocks;
all_predecessors.remove(block);
all_predecessors.remove(&self.pre_header());

// Need to accurately determine whether the current block is dependent on any blocks between
// the current block and the loop header
for predecessor in all_predecessors {
if predecessor == loop_.header {
continue;
}
if self.is_control_dependent(predecessor, *block) {
self.current_block_control_dependent = true;
break;
}
}

for instruction_id in self.inserter.function.dfg[*block].take_instructions() {
self.transform_to_unchecked_from_loop_bounds(instruction_id);

Expand Down Expand Up @@ -159,11 +191,34 @@ impl<'f> LoopInvariantContext<'f> {
}
self.extend_values_defined_in_loop_and_invariants(instruction_id, hoist_invariant);
}

self.current_block_control_dependent = false;
}

self.set_induction_var_bounds(loop_, false);
}

/// Jeanne Ferrante, Karl J. Ottenstein, and Joe D. Warren. 1987.
/// The program dependence graph and its use in optimization. ACM
/// Trans. Program. Lang. Syst. 9, 3 (July 1987), 319–349.
/// https://doi.org/10.1145/24039.24041
///
/// Definition 3 from the linked paper above.
fn is_control_dependent(&mut self, parent_block: BasicBlockId, block: BasicBlockId) -> bool {
let mut all_predecessors = Loop::find_blocks_in_loop(parent_block, block, &self.cfg).blocks;
all_predecessors.remove(&parent_block);
all_predecessors.remove(&block);

let mut first_control_cond = false;
for predecessor in all_predecessors {
if self.post_dom.dominates(predecessor, block) {
first_control_cond = true;
break;
}
}
first_control_cond && !self.post_dom.dominates(block, parent_block)
}

/// Gather the variables declared within the loop
fn set_values_defined_in_loop(&mut self, loop_: &Loop) {
// Clear any values that may be defined in previous loops, as the context is per function.
Expand Down Expand Up @@ -228,6 +283,8 @@ impl<'f> LoopInvariantContext<'f> {

let can_be_deduplicated = instruction.can_be_deduplicated(self.inserter.function, false)
|| matches!(instruction, Instruction::MakeArray { .. })
// TODO: improve this control dependence check
|| (!matches!(instruction, Instruction::Call { .. }) && instruction.requires_acir_gen_predicate(&self.inserter.function.dfg) && !self.current_block_control_dependent)
|| self.can_be_deduplicated_from_loop_bound(&instruction);

is_loop_invariant && can_be_deduplicated
Expand Down Expand Up @@ -901,7 +958,7 @@ mod test {
#[test]
fn do_not_hoist_unsafe_div() {
// This test is similar to `nested_loop_invariant_code_motion`, the operation
// in question we are trying to hoist is `v9 = div i32 10, v0`.
// in question we are trying to hoist is `v7 = div i32 10, v0`.
// Check that the lower bound of the outer loop it checked and that we not
// hoist an operation that can potentially error with a division by zero.
let src = "
Expand Down Expand Up @@ -995,3 +1052,81 @@ mod test {
assert_normalized_ssa_equals(ssa, expected);
}
}

#[cfg(test)]
mod control_dependence {
use crate::ssa::{opt::assert_normalized_ssa_equals, ssa_gen::Ssa};

#[test]
fn do_not_hoist_unsafe_mul_in_control_dependent_block() {
let src = "
brillig(inline) fn main f0 {
b0(v0: u32, v1: u32):
v4 = eq v0, u32 5
jmp b1(u32 0)
b1(v2: u32):
v7 = lt v2, u32 4
jmpif v7 then: b2, else: b3
b2():
jmpif v4 then: b4, else: b5
b3():
return
b4():
v8 = mul v0, v1
constrain v8 == u32 12
jmp b5()
b5():
v11 = unchecked_add v2, u32 1
jmp b1(v11)
}
";

let ssa = Ssa::from_str(src).unwrap();

let ssa = ssa.loop_invariant_code_motion();
assert_normalized_ssa_equals(ssa, src);
}

#[test]
fn hoist_safe_mul_that_is_not_control_dependent() {
let src = "
brillig(inline) fn main f0 {
b0(v0: u32, v1: u32):
jmp b1(u32 0)
b1(v2: u32):
v3 = lt v2, u32 4
jmpif v3 then: b2, else: b3
b2():
v6 = mul v0, v1
v7 = mul v6, v0
constrain v7 == u32 12
v10 = unchecked_add v2, u32 1
jmp b1(v10)
b3():
return
}
";
let ssa = Ssa::from_str(src).unwrap();
let ssa = ssa.loop_invariant_code_motion();

let expected = "
brillig(inline) fn main f0 {
b0(v0: u32, v1: u32):
v3 = mul v0, v1
v4 = mul v3, v0
jmp b1(u32 0)
b1(v2: u32):
v7 = lt v2, u32 4
jmpif v7 then: b2, else: b3
b2():
constrain v4 == u32 12
v10 = unchecked_add v2, u32 1
jmp b1(v10)
b3():
return
}
";

assert_normalized_ssa_equals(ssa, expected);
}
}
2 changes: 1 addition & 1 deletion compiler/noirc_evaluator/src/ssa/opt/unrolling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
impl Loop {
/// Return each block that is in a loop starting in the given header block.
/// Expects back_edge_start -> header to be the back edge of the loop.
fn find_blocks_in_loop(
pub(crate) fn find_blocks_in_loop(
header: BasicBlockId,
back_edge_start: BasicBlockId,
cfg: &ControlFlowGraph,
Expand Down Expand Up @@ -1038,7 +1038,7 @@
use super::{BoilerplateStats, Loops, is_new_size_ok};

/// Tries to unroll all loops in each SSA function once, calling the `Function` directly,
/// bypassing the iterative loop done by the SSA which does further optimisations.

Check warning on line 1041 in compiler/noirc_evaluator/src/ssa/opt/unrolling.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (optimisations)
///
/// If any loop cannot be unrolled, it is left as-is or in a partially unrolled state.
fn try_unroll_loops(mut ssa: Ssa) -> (Ssa, Vec<RuntimeError>) {
Expand Down
13 changes: 7 additions & 6 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
"endianness",
"envrc",
"EXPONENTIATE",
"Ferrante",
"flamegraph",
"Flamegraph",
"flamegraphs",
"flate",
Expand Down Expand Up @@ -123,10 +125,10 @@
"impls",
"indexmap",
"injective",
"interners",
"Inlines",
"instrumenter",
"interner",
"interners",
"intrinsics",
"jmp",
"jmpif",
Expand All @@ -140,6 +142,7 @@
"krate",
"libc",
"Linea",
"lookback",
"losslessly",
"lvalue",
"Maddiaa",
Expand Down Expand Up @@ -174,8 +177,9 @@
"nomicfoundation",
"noncanonical",
"nouner",
"oneshot",
"oneof",
"oneshot",
"Ottenstein",
"overflowing",
"pedersen",
"peekable",
Expand Down Expand Up @@ -269,10 +273,7 @@
"whitespaces",
"YOLO",
"zkhash",
"zshell",
"flamegraph",
"flamegraphs",
"lookback"
"zshell"
],
"ignorePaths": [
"./**/node_modules/**",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fn simple_loop(upper_bound: u32, x: u32, y: u32) {
fn loop_with_predicate(upper_bound: u32, x: u32, y: u32) {
for _ in 0..upper_bound {
if x == 5 {
let mut z = U32_MAX * y;
let mut z = x * y;
assert_eq(z, 12);
}
}
Expand Down
Loading