Skip to content

Commit 94952db

Browse files
TomAFrenchjfecher
andauthored
feat: add remove_enable_side_effects SSA pass (#4224)
# Description ## Problem\* Resolves <!-- Link to GitHub Issue --> ## Summary\* The `EnableSideEffects` instruction can hamper dead instruction elimination in the case where it's not covering any instructions which have side effects (and so are affected by the `EnableSideEffects` instruction). In this case we cannot perform any DIE on instructions used to calculate the predicate as the compiler sees their results as being used. This PR adds a new SSA pass which delays inserting `EnableSideEffects` instructions until the next instruction which would be affected by them. This means that if we hit another `EnableSideEffects` instruction first then we can omit the previous one, unlocking more DIE opportunities. ## Additional Context ## Documentation\* Check one: - [ ] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[Exceptional Case]** Documentation to be submitted in a separate PR. # PR Checklist\* - [ ] I have tested the changes locally. - [ ] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --------- Co-authored-by: jfecher <[email protected]>
1 parent 66835da commit 94952db

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

compiler/noirc_evaluator/src/ssa.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pub(crate) fn optimize_into_acir(
6262
// Run mem2reg once more with the flattened CFG to catch any remaining loads/stores
6363
.run_pass(Ssa::mem2reg, "After Mem2Reg:")
6464
.run_pass(Ssa::fold_constants, "After Constant Folding:")
65+
.run_pass(Ssa::remove_enable_side_effects, "After EnableSideEffects removal:")
6566
.run_pass(Ssa::fold_constants_using_constraints, "After Constraint Folding:")
6667
.run_pass(Ssa::dead_instruction_elimination, "After Dead Instruction Elimination:")
6768
.run_pass(Ssa::array_set_optimization, "After Array Set Optimizations:")

compiler/noirc_evaluator/src/ssa/opt/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ mod inlining;
1515
mod mem2reg;
1616
mod rc;
1717
mod remove_bit_shifts;
18+
mod remove_enable_side_effects;
1819
mod simplify_cfg;
1920
mod unrolling;
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! The goal of the "remove enable side effects" optimization pass is to delay any [Instruction::EnableSideEffects]
2+
//! instructions such that they cover the minimum number of instructions possible.
3+
//!
4+
//! The pass works as follows:
5+
//! - Insert instructions until an [Instruction::EnableSideEffects] is encountered, save this [InstructionId].
6+
//! - Continue inserting instructions until either
7+
//! - Another [Instruction::EnableSideEffects] is encountered, if so then drop the previous [InstructionId] in favour
8+
//! of this one.
9+
//! - An [Instruction] with side-effects is encountered, if so then insert the currently saved [Instruction::EnableSideEffects]
10+
//! before the [Instruction]. Continue inserting instructions until the next [Instruction::EnableSideEffects] is encountered.
11+
use std::collections::HashSet;
12+
13+
use acvm::FieldElement;
14+
15+
use crate::ssa::{
16+
ir::{
17+
basic_block::BasicBlockId,
18+
dfg::DataFlowGraph,
19+
function::Function,
20+
instruction::{BinaryOp, Instruction, Intrinsic},
21+
value::Value,
22+
},
23+
ssa_gen::Ssa,
24+
};
25+
26+
impl Ssa {
27+
/// See [`remove_enable_side_effects`][self] module for more information.
28+
#[tracing::instrument(level = "trace", skip(self))]
29+
pub(crate) fn remove_enable_side_effects(mut self) -> Ssa {
30+
for function in self.functions.values_mut() {
31+
remove_enable_side_effects(function);
32+
}
33+
self
34+
}
35+
}
36+
37+
fn remove_enable_side_effects(function: &mut Function) {
38+
let mut context = Context::default();
39+
context.block_queue.push(function.entry_block());
40+
41+
while let Some(block) = context.block_queue.pop() {
42+
if context.visited_blocks.contains(&block) {
43+
continue;
44+
}
45+
46+
context.visited_blocks.insert(block);
47+
context.remove_enable_side_effects_in_block(function, block);
48+
}
49+
}
50+
51+
#[derive(Default)]
52+
struct Context {
53+
visited_blocks: HashSet<BasicBlockId>,
54+
block_queue: Vec<BasicBlockId>,
55+
}
56+
57+
impl Context {
58+
fn remove_enable_side_effects_in_block(
59+
&mut self,
60+
function: &mut Function,
61+
block: BasicBlockId,
62+
) {
63+
let instructions = function.dfg[block].take_instructions();
64+
65+
let mut last_side_effects_enabled_instruction = None;
66+
67+
let mut new_instructions = Vec::with_capacity(instructions.len());
68+
for instruction_id in instructions {
69+
let instruction = &function.dfg[instruction_id];
70+
71+
// If we run into another `Instruction::EnableSideEffects` before encountering any
72+
// instructions with side effects then we can drop the instruction we're holding and
73+
// continue with the new `Instruction::EnableSideEffects`.
74+
if let Instruction::EnableSideEffects { condition } = instruction {
75+
// If we're seeing an `enable_side_effects u1 1` then we want to insert it immediately.
76+
// This is because we want to maximize the effect it will have.
77+
if function
78+
.dfg
79+
.get_numeric_constant(*condition)
80+
.map_or(false, |condition| condition.is_one())
81+
{
82+
new_instructions.push(instruction_id);
83+
last_side_effects_enabled_instruction = None;
84+
continue;
85+
}
86+
87+
last_side_effects_enabled_instruction = Some(instruction_id);
88+
continue;
89+
}
90+
91+
// If we hit an instruction which is affected by the side effects var then we must insert the
92+
// `Instruction::EnableSideEffects` before we insert this new instruction.
93+
if Self::responds_to_side_effects_var(&function.dfg, instruction) {
94+
if let Some(enable_side_effects_instruction_id) =
95+
last_side_effects_enabled_instruction.take()
96+
{
97+
new_instructions.push(enable_side_effects_instruction_id);
98+
}
99+
}
100+
new_instructions.push(instruction_id);
101+
}
102+
103+
*function.dfg[block].instructions_mut() = new_instructions;
104+
105+
self.block_queue.extend(function.dfg[block].successors());
106+
}
107+
108+
fn responds_to_side_effects_var(dfg: &DataFlowGraph, instruction: &Instruction) -> bool {
109+
use Instruction::*;
110+
match instruction {
111+
Binary(binary) => {
112+
if matches!(binary.operator, BinaryOp::Div | BinaryOp::Mod) {
113+
if let Some(rhs) = dfg.get_numeric_constant(binary.rhs) {
114+
rhs == FieldElement::zero()
115+
} else {
116+
true
117+
}
118+
} else {
119+
false
120+
}
121+
}
122+
123+
Cast(_, _)
124+
| Not(_)
125+
| Truncate { .. }
126+
| Constrain(..)
127+
| RangeCheck { .. }
128+
| IncrementRc { .. }
129+
| DecrementRc { .. } => false,
130+
131+
EnableSideEffects { .. }
132+
| ArrayGet { .. }
133+
| ArraySet { .. }
134+
| Allocate
135+
| Store { .. }
136+
| Load { .. } => true,
137+
138+
// Some `Intrinsic`s have side effects so we must check what kind of `Call` this is.
139+
Call { func, .. } => match dfg[*func] {
140+
Value::Intrinsic(intrinsic) => match intrinsic {
141+
Intrinsic::SlicePushBack
142+
| Intrinsic::SlicePushFront
143+
| Intrinsic::SlicePopBack
144+
| Intrinsic::SlicePopFront
145+
| Intrinsic::SliceInsert
146+
| Intrinsic::SliceRemove => true,
147+
148+
Intrinsic::ArrayLen
149+
| Intrinsic::AssertConstant
150+
| Intrinsic::ApplyRangeConstraint
151+
| Intrinsic::StrAsBytes
152+
| Intrinsic::ToBits(_)
153+
| Intrinsic::ToRadix(_)
154+
| Intrinsic::BlackBox(_)
155+
| Intrinsic::FromField
156+
| Intrinsic::AsField
157+
| Intrinsic::AsSlice => false,
158+
},
159+
160+
// We must assume that functions contain a side effect as we cannot inspect more deeply.
161+
Value::Function(_) => true,
162+
163+
_ => false,
164+
},
165+
}
166+
}
167+
}

0 commit comments

Comments
 (0)