Skip to content

Commit

Permalink
Merge 17644c8 into 713df69
Browse files Browse the repository at this point in the history
  • Loading branch information
jfecher authored Nov 18, 2024
2 parents 713df69 + 17644c8 commit fd530e9
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 334 deletions.
3 changes: 2 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use super::{
dfg::{CallStack, DataFlowGraph},
function::Function,
map::Id,
types::{NumericType, Type},
types::Type,
value::{Value, ValueId},
};

Expand Down Expand Up @@ -413,6 +413,7 @@ impl Instruction {

Constrain(..)
| EnableSideEffectsIf { .. }
| Store { .. }
| IncrementRc { .. }
| DecrementRc { .. }
| RangeCheck { .. } => false,
Expand Down
6 changes: 3 additions & 3 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use acvm::{acir::AcirField, FieldElement};
use serde::{Deserialize, Serialize};

use super::{
DataFlowGraph, Instruction, InstructionResultType, NumericType, SimplifyResult, Type, ValueId,
};
use crate::ssa::ir::types::NumericType;

use super::{DataFlowGraph, Instruction, InstructionResultType, SimplifyResult, Type, ValueId};

/// Binary Operations allowed in the IR.
/// Aside from the comparison operators (Eq and Lt), all operators
Expand Down
4 changes: 3 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/instruction/cast.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use acvm::{acir::AcirField, FieldElement};
use num_bigint::BigUint;

use super::{DataFlowGraph, Instruction, NumericType, SimplifyResult, Type, Value, ValueId};
use crate::ssa::ir::types::NumericType;

use super::{DataFlowGraph, Instruction, SimplifyResult, Type, Value, ValueId};

/// Try to simplify this cast instruction. If the instruction can be simplified to a known value,
/// that value is returned. Otherwise None is returned.
Expand Down
4 changes: 3 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/post_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl PostOrder {
stack.push((Visit::Last, block_id));
// Stack successors for visiting. Because items are taken from the top of the
// stack, we push the item that's due for a visit first to the top.
for successor_id in func.dfg[block_id].successors().rev() {
for successor_id in func.dfg[block_id].successors() {
if !visited.contains(&successor_id) {
// This not visited check would also be cover by the next
// iteration, but checking here two saves an iteration per successor.
Expand Down Expand Up @@ -147,4 +147,6 @@ mod tests {
let block_a_id = func.entry_block();
assert_eq!(post_order.0, [block_d_id, block_f_id, block_e_id, block_b_id, block_a_id]);
}

// TODO: Add `loop` test
}
175 changes: 130 additions & 45 deletions compiler/noirc_evaluator/src/ssa/opt/die.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use noirc_errors::Location;
use crate::ssa::{
ir::{
basic_block::{BasicBlock, BasicBlockId},
cfg::ControlFlowGraph,
dfg::DataFlowGraph,
function::Function,
instruction::{BinaryOp, Instruction, InstructionId, Intrinsic},
Expand All @@ -17,69 +18,106 @@ use crate::ssa::{
ssa_gen::{Ssa, SSA_WORD_SIZE},
};

use super::rc::{pop_rc_for, RcInstruction};
use super::{
mem2reg::{AliasSet, StoreInstructionAliases},
rc::{pop_rc_for, RcInstruction},
};

impl Ssa {
/// Performs Dead Instruction Elimination (DIE) to remove any instructions with
/// unused results.
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) fn dead_instruction_elimination(mut self) -> Ssa {
for function in self.functions.values_mut() {
function.dead_instruction_elimination(true);
Context::new(function).dead_instruction_elimination(function, true);
}
self
}
}

impl Function {
pub(crate) fn dce_with_store_aliases(function: &mut Function, aliases: StoreInstructionAliases) {
let mut context = Context::new(function);
context.store_aliases = Some(aliases);
context.dead_instruction_elimination(function, true);
}

/// Per function context for tracking unused values and which instructions to remove.
struct Context {
used_values: HashSet<ValueId>,
instructions_to_remove: HashSet<InstructionId>,

/// IncrementRc & DecrementRc instructions must be revisited after the main DIE pass since
/// they technically contain side-effects but we still want to remove them if their
/// `value` parameter is not used elsewhere.
rc_instructions: Vec<(InstructionId, BasicBlockId)>,

/// If we have information on which store instructions may alias other references we
/// can remove unused store instructions as well. This information is given by mem2reg
/// and is on each store instruction since which ValueIds are aliased would change over time
/// (so we cannot index by ValueId).
store_aliases: Option<StoreInstructionAliases>,

cfg: ControlFlowGraph,

visited_blocks: HashSet<BasicBlockId>,
}

impl Context {
fn new(function: &Function) -> Self {
Self {
used_values: HashSet::default(),
instructions_to_remove: HashSet::default(),
rc_instructions: Vec::new(),
store_aliases: None,
visited_blocks: HashSet::default(),
cfg: ControlFlowGraph::with_function(function),
}
}

/// Removes any unused instructions in the reachable blocks of the given function.
///
/// The blocks of the function are iterated in post order, such that any blocks containing
/// instructions that reference results from an instruction in another block are evaluated first.
/// If we did not iterate blocks in this order we could not safely say whether or not the results
/// of its instructions are needed elsewhere.
pub(crate) fn dead_instruction_elimination(&mut self, insert_out_of_bounds_checks: bool) {
let mut context = Context::default();
for call_data in &self.dfg.data_bus.call_data {
context.mark_used_instruction_results(&self.dfg, call_data.array_id);
fn dead_instruction_elimination(
mut self,
function: &mut Function,
insert_out_of_bounds_checks: bool,
) {
for call_data in &function.dfg.data_bus.call_data {
Self::mark_used_instruction_results(
&function.dfg,
call_data.array_id,
&mut self.used_values,
);
}

let mut inserted_out_of_bounds_checks = false;

let blocks = PostOrder::with_function(self);
let blocks = PostOrder::with_function(function);
for block in blocks.as_slice() {
inserted_out_of_bounds_checks |= context.remove_unused_instructions_in_block(
self,
inserted_out_of_bounds_checks |= self.remove_unused_instructions_in_block(
function,
*block,
insert_out_of_bounds_checks,
);
self.visited_blocks.insert(*block);
}

// If we inserted out of bounds check, let's run the pass again with those new
// instructions (we don't want to remove those checks, or instructions that are
// dependencies of those checks)
if inserted_out_of_bounds_checks {
self.dead_instruction_elimination(false);
return;
let store_aliases = self.store_aliases;
self = Context::new(function);
self.store_aliases = store_aliases;
return self.dead_instruction_elimination(function, false);
}

context.remove_rc_instructions(&mut self.dfg);
self.remove_rc_instructions(&mut function.dfg);
}
}

/// Per function context for tracking unused values and which instructions to remove.
#[derive(Default)]
struct Context {
used_values: HashSet<ValueId>,
instructions_to_remove: HashSet<InstructionId>,

/// IncrementRc & DecrementRc instructions must be revisited after the main DIE pass since
/// they technically contain side-effects but we still want to remove them if their
/// `value` parameter is not used elsewhere.
rc_instructions: Vec<(InstructionId, BasicBlockId)>,
}

impl Context {
/// Steps backwards through the instruction of the given block, amassing a set of used values
/// as it goes, and at the same time marking instructions for removal if they haven't appeared
/// in the set thus far.
Expand All @@ -106,34 +144,40 @@ impl Context {
let block = &function.dfg[block_id];
self.mark_terminator_values_as_used(function, block);

let instructions_len = block.instructions().len();

let mut rc_tracker = RcTracker::default();

// Indexes of instructions that might be out of bounds.
// We'll remove those, but before that we'll insert bounds checks for them.
let mut possible_index_out_of_bounds_indexes = Vec::new();

for (instruction_index, instruction_id) in block.instructions().iter().rev().enumerate() {
for (instruction_index, instruction_id) in block.instructions().iter().enumerate().rev() {
let instruction = &function.dfg[*instruction_id];

if self.is_unused(*instruction_id, function) {
if self.is_unused(*instruction_id, function, block_id) {
self.instructions_to_remove.insert(*instruction_id);

if insert_out_of_bounds_checks
&& instruction_might_result_in_out_of_bounds(function, instruction)
{
possible_index_out_of_bounds_indexes
.push(instructions_len - instruction_index - 1);
.push(instruction_index);
}
} else {
use Instruction::*;
if matches!(instruction, IncrementRc { .. } | DecrementRc { .. }) {
self.rc_instructions.push((*instruction_id, block_id));
} else {
instruction.for_each_value(|value| {
self.mark_used_instruction_results(&function.dfg, value);
Self::mark_used_instruction_results(
&function.dfg,
value,
&mut self.used_values,
);
});

if matches!(instruction, Store { .. }) {
self.mark_used_store_aliases(&function.dfg, *instruction_id);
}
}
}

Expand Down Expand Up @@ -169,12 +213,21 @@ impl Context {
///
/// An instruction can be removed as long as it has no side-effects, and none of its result
/// values have been referenced.
fn is_unused(&self, instruction_id: InstructionId, function: &Function) -> bool {
fn is_unused(
&self,
instruction_id: InstructionId,
function: &Function,
block: BasicBlockId,
) -> bool {
let instruction = &function.dfg[instruction_id];

if instruction.can_eliminate_if_unused(function) {
let results = function.dfg.instruction_results(instruction_id);
results.iter().all(|result| !self.used_values.contains(result))
results.iter().all(|result| {
!self.used_values.contains(result)
})
} else if let Instruction::Store { address, .. } = instruction {
self.store_is_unused(instruction_id, *address, block)
} else if let Instruction::Call { func, arguments } = instruction {
// TODO: make this more general for instructions which don't have results but have side effects "sometimes" like `Intrinsic::AsWitness`
let as_witness_id = function.dfg.get_intrinsic(Intrinsic::AsWitness);
Expand All @@ -185,21 +238,56 @@ impl Context {
}
}

fn store_is_unused(
&self,
instruction_id: InstructionId,
address: ValueId,
block: BasicBlockId,
) -> bool {
if !self.cfg.successors(block).all(|block| self.visited_blocks.contains(&block)) {
return false;
}

if let Some(aliases) = Self::get_store_aliases(&self.store_aliases, instruction_id) {
aliases.any(|alias| self.used_values.contains(&alias)) == Some(false)
&& !self.used_values.contains(&address)
} else {
false
}
}

/// Adds values referenced by the terminator to the set of used values.
fn mark_terminator_values_as_used(&mut self, function: &Function, block: &BasicBlock) {
block.unwrap_terminator().for_each_value(|value| {
self.mark_used_instruction_results(&function.dfg, value);
Self::mark_used_instruction_results(&function.dfg, value, &mut self.used_values);
});
}

/// Inspects a value and marks all instruction results as used.
fn mark_used_instruction_results(&mut self, dfg: &DataFlowGraph, value_id: ValueId) {
fn mark_used_instruction_results(dfg: &DataFlowGraph, value_id: ValueId, used_values: &mut HashSet<ValueId>) {
let value_id = dfg.resolve(value_id);
if matches!(&dfg[value_id], Value::Instruction { .. } | Value::Param { .. }) {
self.used_values.insert(value_id);
used_values.insert(value_id);
}
}

/// Marks each alias of `address` for the given store instruction as used
fn mark_used_store_aliases(&mut self, dfg: &DataFlowGraph, instruction: InstructionId) {
if let Some(aliases) = Self::get_store_aliases(&self.store_aliases, instruction) {
aliases.for_each(|alias| {
Self::mark_used_instruction_results(dfg, alias, &mut self.used_values)
})
}
}

// unknown stores mess this up...
fn get_store_aliases(
store_aliases: &Option<StoreInstructionAliases>,
instruction: InstructionId,
) -> Option<&AliasSet> {
store_aliases.as_ref().and_then(|aliases| aliases.get(&instruction))
}

fn remove_rc_instructions(self, dfg: &mut DataFlowGraph) {
for (rc, block) in self.rc_instructions {
let value = match &dfg[rc] {
Expand Down Expand Up @@ -282,11 +370,9 @@ impl Context {

let call_stack = function.dfg.get_call_stack(instruction_id);

let (lhs, rhs) = if function.dfg.get_numeric_constant(*index).is_some() {
let lhs = if function.dfg.get_numeric_constant(*index).is_some() {
// If we are here it means the index is known but out of bounds. That's always an error!
let false_const = function.dfg.make_constant(false.into(), Type::bool());
let true_const = function.dfg.make_constant(true.into(), Type::bool());
(false_const, true_const)
function.dfg.make_constant(false.into(), Type::bool())
} else {
// `index` will be relative to the flattened array length, so we need to take that into account
let array_length = function.dfg.type_of_value(*array).flattened_size();
Expand All @@ -309,11 +395,10 @@ impl Context {
None,
call_stack.clone(),
);
let is_index_out_of_bounds = is_index_out_of_bounds.first();
let true_const = function.dfg.make_constant(true.into(), Type::bool());
(is_index_out_of_bounds, true_const)
is_index_out_of_bounds.first()
};

let rhs = function.dfg.make_constant(true.into(), Type::bool());
let (lhs, rhs) = apply_side_effects(
side_effects_condition,
lhs,
Expand Down
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/src/ssa/opt/inlining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ impl InlineContext {

context.blocks.insert(context.source_function.entry_block(), entry_block);
context.inline_blocks(ssa);

// translate databus values
let databus = entry_point.dfg.data_bus.map_values(|t| context.translate_value(t));

Expand Down
Loading

0 comments on commit fd530e9

Please sign in to comment.