Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
2 changes: 1 addition & 1 deletion .github/benchmark_projects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ projects:
ref: *AZ_COMMIT
path: noir-projects/noir-protocol-circuits/crates/rollup-tx-base-public
num_runs: 5
compilation-timeout: 100
compilation-timeout: 120
execution-timeout: 0.75
compilation-memory-limit: 8000
execution-memory-limit: 600
Expand Down
17 changes: 12 additions & 5 deletions compiler/noirc_evaluator/src/ssa/ir/dfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,8 @@ impl DataFlowGraph {
if self[id] == instruction {
// Just (re)insert into the block, no need to redefine.
self.blocks[block].insert_instruction(id);
return InsertInstructionResult::Results(
id,
self.instruction_results(id),
);
let results = self.instruction_results(id);
return InsertInstructionResult::Results(id, results);
}
}
}
Expand Down Expand Up @@ -588,12 +586,21 @@ impl DataFlowGraph {

/// Remove an instruction by replacing it with a `Noop` instruction.
/// Doing this avoids shifting over each instruction after this one in its block's instructions vector.
#[allow(unused)]
pub(crate) fn remove_instruction(&mut self, instruction: InstructionId) {
self.instructions[instruction] = Instruction::Noop;
self.results.insert(instruction, smallvec::SmallVec::new());
}

/// Remove instructions for which the `keep` functions returns `false`.
pub(crate) fn retain_instructions(&mut self, keep: impl Fn(InstructionId) -> bool) {
for i in 0..self.instructions.len() {
let id = InstructionId::new(i as u32);
if !keep(id) {
self.remove_instruction(id);
Comment thread
jfecher marked this conversation as resolved.
}
}
}

/// Add a parameter to the given block
pub(crate) fn add_block_parameter(&mut self, block_id: BasicBlockId, typ: Type) -> ValueId {
let block = &mut self.blocks[block_id];
Expand Down
33 changes: 27 additions & 6 deletions compiler/noirc_evaluator/src/ssa/ir/function_inserter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl<'f> FunctionInserter<'f> {
/// ValueId that was passed in.
pub(crate) fn resolve(&self, value: ValueId) -> ValueId {
match self.values.get(&value) {
Some(value) => self.resolve(*value),
Some(new_value) => self.resolve(*new_value),
None => value,
}
}
Expand All @@ -41,6 +41,8 @@ impl<'f> FunctionInserter<'f> {
// existing entries, but we should never have a value in the map referring to itself anyway.
self.values.remove(&key);
} else {
#[cfg(debug_assertions)]
self.validate_map_value(key, value);
self.values.entry(key).or_insert(value);
}
}
Expand All @@ -50,10 +52,20 @@ impl<'f> FunctionInserter<'f> {
if key == value {
self.values.remove(&key);
} else {
#[cfg(debug_assertions)]
self.validate_map_value(key, value);
self.values.insert(key, value);
}
}

/// Sanity check that we are not creating cycles.
#[cfg(debug_assertions)]
fn validate_map_value(&self, key: ValueId, value: ValueId) {
if let Some(value_of_value) = self.values.get(&value) {
assert!(*value_of_value != key, "reflexive mapping: {key} <-> {value}");
Comment thread
aakoshh marked this conversation as resolved.
Outdated
}
}

/// Get an instruction and make sure all the values in it are freshly resolved.
pub(crate) fn map_instruction(&mut self, id: InstructionId) -> (Instruction, CallStackId) {
let mut instruction = self.function.dfg[id].clone();
Expand Down Expand Up @@ -84,39 +96,46 @@ impl<'f> FunctionInserter<'f> {
self.function.dfg.data_bus = data_bus;
}

/// Push a new instruction to the given block and return its new `InstructionId`.
/// Push an instruction by ID, after re-mapping the values in it, to the given block and return its new `InstructionId`.
/// If the instruction was simplified out of the program, `None` is returned.
pub(crate) fn push_instruction(
&mut self,
id: InstructionId,
block: BasicBlockId,
allow_reinsert: bool,
) -> Option<InstructionId> {
let (instruction, location) = self.map_instruction(id);

match self.push_instruction_value(instruction, id, block, location) {
match self.push_instruction_value(instruction, id, block, location, allow_reinsert) {
InsertInstructionResult::Results(new_id, _) => Some(new_id),
_ => None,
}
}

/// Push a instruction that already exists with an ID, given as an instance with potential modification already applied to it.
///
/// If `allow_insert` is set, we consider reinserting the instruction as it is, if it hasn't changed and cannot be simplified.
/// Consider using this if we are re-inserting when we take instructions from a block and re-insert them into the same block.
pub(crate) fn push_instruction_value(
&mut self,
instruction: Instruction,
id: InstructionId,
block: BasicBlockId,
call_stack: CallStackId,
allow_reinsert: bool,
) -> InsertInstructionResult<'_> {
let results = self.function.dfg.instruction_results(id).to_vec();

let ctrl_typevars = instruction
.requires_ctrl_typevars()
.then(|| vecmap(&results, |result| self.function.dfg.type_of_value(*result)));

let new_results = self.function.dfg.insert_instruction_and_results(
let new_results = self.function.dfg.insert_instruction_and_results_if_simplified(
instruction,
block,
ctrl_typevars,
call_stack,
allow_reinsert.then_some(id),
);

Self::insert_new_instruction_results(&mut self.values, &results, &new_results);
Expand All @@ -125,14 +144,16 @@ impl<'f> FunctionInserter<'f> {

/// Modify the values HashMap to remember the mapping between an instruction result's previous
/// ValueId (from the source_function) and its new ValueId in the destination function.
pub(crate) fn insert_new_instruction_results(
fn insert_new_instruction_results(
values: &mut HashMap<ValueId, ValueId>,
old_results: &[ValueId],
new_results: &InsertInstructionResult,
) {
assert_eq!(old_results.len(), new_results.len());
for i in 0..old_results.len() {
values.insert(old_results[i], new_results[i]);
if old_results[i] != new_results[i] {
values.insert(old_results[i], new_results[i]);
}
}
}

Expand Down
16 changes: 12 additions & 4 deletions compiler/noirc_evaluator/src/ssa/ir/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,10 +688,18 @@ impl Instruction {
else_value: f(*else_value),
}
}
Instruction::MakeArray { elements, typ } => Instruction::MakeArray {
elements: elements.iter().copied().map(f).collect(),
typ: typ.clone(),
},
Instruction::MakeArray { elements, typ } => {
// Going in this elaborate way to avoid having to clone the Arcs in the im::Vec if nothing changed.
Comment thread
aakoshh marked this conversation as resolved.
Outdated
let mut elements = elements.clone();
for i in 0..elements.len() {
let old = elements[i];
let new = f(old);
if new != old {
elements[i] = new;
}
}
Instruction::MakeArray { elements, typ: typ.clone() }
}
Instruction::Noop => Instruction::Noop,
}
}
Expand Down
5 changes: 5 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ impl<T> DenseMap<T> {
let ids_iter = (0..self.storage.len() as u32).map(|idx| Id::new(idx));
ids_iter.zip(self.storage.iter())
}

/// Length of the underlying storage.
pub(crate) fn len(&self) -> usize {
self.storage.len()
}
}

impl<T> Default for DenseMap<T> {
Expand Down
5 changes: 3 additions & 2 deletions compiler/noirc_evaluator/src/ssa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ pub fn primary_passes(options: &SsaEvaluatorOptions) -> Vec<SsaPass<'_>> {
SsaPass::new(Ssa::expand_signed_math, "Expand signed math"),
SsaPass::new(Ssa::simplify_cfg, "Simplifying"),
SsaPass::new(Ssa::flatten_cfg, "Flattening"),
// Run mem2reg once more with the flattened CFG to catch any remaining loads/stores
SsaPass::new(Ssa::mem2reg, "Mem2Reg"),
// Run mem2reg once more with the flattened CFG to catch any remaining loads/stores,
// then try to free memory before inlining, which involves copying a instructions.
SsaPass::new(Ssa::mem2reg, "Mem2Reg").and_then(Ssa::remove_unused_instructions),
// Run the inlining pass again to handle functions with `InlineType::NoPredicates`.
// Before flattening is run, we treat functions marked with the `InlineType::NoPredicates` as an entry point.
// This pass must come immediately following `mem2reg` as the succeeding passes
Expand Down
9 changes: 7 additions & 2 deletions compiler/noirc_evaluator/src/ssa/opt/flatten_cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,13 @@ impl<'f> Context<'f> {
let instruction = self.handle_instruction_side_effects(instruction, call_stack);

let instruction_is_allocate = matches!(&instruction, Instruction::Allocate);
let results =
self.inserter.push_instruction_value(instruction, id, self.target_block, call_stack);
let results = self.inserter.push_instruction_value(
instruction,
id,
self.target_block,
call_stack,
true,
);

// Remember an allocate was created local to this branch so that we do not try to merge store
// values across branches for it later.
Expand Down
46 changes: 34 additions & 12 deletions compiler/noirc_evaluator/src/ssa/opt/inlining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
//! within the function caller. If all function calls are known, there will only
//! be a single function remaining when the pass finishes.

use crate::{errors::RuntimeError, ssa::visit_once_deque::VisitOnceDeque};
use std::collections::HashSet;

use crate::{
errors::RuntimeError,
ssa::{opt::inlining::inline_info::compute_bottom_up_order, visit_once_deque::VisitOnceDeque},
};
use acvm::acir::AcirField;
use im::HashMap;
use iter_extended::vecmap;
Expand Down Expand Up @@ -79,14 +84,20 @@ impl Ssa {
let num_functions_before = self.functions.len();

let call_graph = CallGraph::from_ssa_weighted(&self);

let inline_infos = compute_inline_infos(
&self,
&call_graph,
inline_no_predicates_functions,
small_function_max_instructions,
aggressiveness,
);
self = Self::inline_functions_inner(self, &inline_infos)?;

// Bottom-up order, starting with the "leaf" functions.
let bottom_up = compute_bottom_up_order(&self, &call_graph);
let bottom_up = vecmap(bottom_up, |(id, _)| id);

self = Self::inline_functions_inner(self, &inline_infos, &bottom_up)?;

let num_functions_after = self.functions.len();
if num_functions_after == num_functions_before {
Expand All @@ -97,28 +108,39 @@ impl Ssa {
Ok(self)
}

fn inline_functions_inner(mut self, inline_infos: &InlineInfos) -> Result<Ssa, RuntimeError> {
let inline_targets = inline_infos.iter().filter_map(|(id, info)| {
let dfg = &self.functions[id].dfg;
info.is_inline_target(dfg).then_some(*id)
});
/// Inline entry points in the order of appearance in `inline_infos`, assuming it goes in bottom-up order.
fn inline_functions_inner(
mut self,
inline_infos: &InlineInfos,
bottom_up: &[FunctionId],
) -> Result<Ssa, RuntimeError> {
let inline_targets = bottom_up
.iter()
.filter_map(|id| {
let info = inline_infos.get(id)?;
let dfg = &self.functions[id].dfg;
info.is_inline_target(dfg).then_some(*id)
})
.collect::<Vec<_>>();

let should_inline_call = |callee: &Function| -> bool {
// We defer to the inline info computation to determine whether a function should be inlined
InlineInfo::should_inline(inline_infos, callee.id())
};

// NOTE: Functions are processed independently of each other, with the final mapping replacing the original,
// instead of inlining the "leaf" functions, moving up towards the entry point.
let mut new_functions = std::collections::BTreeMap::new();
// We are going bottom up, so hopefully we can inline leaf functions into their callers and retain less memory.
let mut new_functions = HashSet::new();
for entry_point in inline_targets {
let function = &self.functions[&entry_point];
let inlined = function.inlined(&self, &should_inline_call)?;
assert_eq!(inlined.id(), entry_point);
new_functions.insert(entry_point, inlined);
self.functions.insert(entry_point, inlined);
new_functions.insert(entry_point);
}

self.functions = new_functions;
// Drop functions that weren't inline targets.
self.functions.retain(|id, _| new_functions.contains(id));

Ok(self)
}
}
Expand Down
6 changes: 3 additions & 3 deletions compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ impl<'f> LoopInvariantContext<'f> {
self.can_hoist_invariant(&loop_context, &block_context, instruction_id);

if hoist_invariant {
self.inserter.push_instruction(instruction_id, pre_header);
self.inserter.push_instruction(instruction_id, pre_header, false);

// If we are hoisting a MakeArray instruction,
// we need to issue an extra inc_rc in case they are mutated afterward.
Expand All @@ -452,7 +452,7 @@ impl<'f> LoopInvariantContext<'f> {
if !block_context.is_impure {
block_context.is_impure = dfg[instruction_id].has_side_effects(dfg);
}
self.inserter.push_instruction(instruction_id, *block);
self.inserter.push_instruction(instruction_id, *block, true);
}

// We will have new IDs after pushing instructions.
Expand Down Expand Up @@ -748,7 +748,7 @@ impl<'f> LoopInvariantContext<'f> {

for block in block_order {
for instruction_id in self.inserter.function.dfg[block].take_instructions() {
self.inserter.push_instruction(instruction_id, block);
self.inserter.push_instruction(instruction_id, block, true);
}
self.inserter.map_terminator_in_place(block);
}
Expand Down
17 changes: 16 additions & 1 deletion compiler/noirc_evaluator/src/ssa/opt/mem2reg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ struct PerFunctionContext<'f> {
/// from the middle of Vecs many times will be slower than a single call to `retain`.
instructions_to_remove: HashSet<InstructionId>,

/// All instructions analyzed so far in the function.
/// Anything new can be reinserted, while things that appear repeatedly have to be cloned.
instructions_analyzed: HashSet<InstructionId>,

/// Track a value's last load across all blocks.
/// If a value is not used in anymore loads we can remove the last store to that value.
last_loads: HashSet<ValueId>,
Expand All @@ -159,6 +163,7 @@ impl<'f> PerFunctionContext<'f> {
inserter: FunctionInserter::new(function),
blocks: BTreeMap::new(),
instructions_to_remove: HashSet::default(),
instructions_analyzed: HashSet::default(),
last_loads: HashSet::default(),
aliased_references: HashMap::default(),
instruction_input_references: HashSet::default(),
Expand Down Expand Up @@ -419,7 +424,17 @@ impl<'f> PerFunctionContext<'f> {
// and we need to mark those references as used to keep their stores alive.
let (instruction, loc) = self.inserter.map_instruction(instruction_id);

match self.inserter.push_instruction_value(instruction, instruction_id, block_id, loc) {
// We track which instructions can be removed by ID; if we allowed the same ID to appear multiple times
// in a block then we could not tell them apart. When we see something the first time we can reuse it.
let allow_reinsert = self.instructions_analyzed.insert(instruction_id);

match self.inserter.push_instruction_value(
instruction,
instruction_id,
block_id,
loc,
allow_reinsert,
) {
InsertInstructionResult::Results(id, _) => {
self.analyze_possibly_simplified_instruction(references, id, false);
}
Expand Down
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/src/ssa/opt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod remove_if_else;
mod remove_truncate_after_range_check;
mod remove_unreachable_functions;
mod remove_unreachable_instructions;
mod remove_unused_instructions;
mod simple_optimization;
mod simplify_cfg;
mod unrolling;
Expand Down
Loading
Loading