Skip to content

Commit f2cef7c

Browse files
author
AztecBot
committed
feat(ssa): Option to set the maximum acceptable Brillig bytecode increase in unrolling (noir-lang/noir#6641)
feat: Sync from aztec-packages (noir-lang/noir#6656) chore: refactor poseidon2 (noir-lang/noir#6655) fix: correct types returned by constant EC operations simplified within SSA (noir-lang/noir#6652) feat: Sync from aztec-packages (noir-lang/noir#6634) fix: used signed division for signed modulo (noir-lang/noir#6635) fix(ssa): don't deduplicate constraints in blocks that are not dominated (noir-lang/noir#6627) chore: pin foundry version in CI (noir-lang/noir#6642) feat(ssa): Deduplicate intrinsics with predicates (noir-lang/noir#6615) chore: improve error message of `&T` (noir-lang/noir#6633) fix: LSP code action wasn't triggering on beginning or end of identifier (noir-lang/noir#6616) chore!: remove `ec` module from stdlib (noir-lang/noir#6612) fix(LSP): use generic self type to narrow down methods to complete (noir-lang/noir#6617) fix!: Disallow `#[export]` on associated methods (noir-lang/noir#6626) chore: redo typo PR by donatik27 (noir-lang/noir#6575) chore: redo typo PR by Dimitrolito (noir-lang/noir#6614) feat: simplify `jmpif`s by reversing branches if condition is negated (noir-lang/noir#5891) fix: Do not warn on unused functions marked with #[export] (noir-lang/noir#6625) chore: Add panic for compiler error described in #6620 (noir-lang/noir#6621) feat(perf): Track last loads per block in mem2reg and remove them if possible (noir-lang/noir#6088) fix(ssa): Track all local allocations during flattening (noir-lang/noir#6619) feat(comptime): Implement blackbox functions in comptime interpreter (noir-lang/noir#6551) chore: derive PartialEq and Hash for FieldElement (noir-lang/noir#6610) chore: ignore almost-empty directories in nargo_cli tests (noir-lang/noir#6611) chore: remove temporary allocations from `num_bits` (noir-lang/noir#6600) chore: Release Noir(1.0.0-beta.0) (noir-lang/noir#6562) feat: Add `array_refcount` and `slice_refcount` builtins for debugging (noir-lang/noir#6584) chore!: Require types of globals to be specified (noir-lang/noir#6592) fix: don't report visibility errors when elaborating comptime value (noir-lang/noir#6498) fix: preserve newlines between comments when formatting statements (noir-lang/noir#6601) fix: parse a bit more SSA stuff (noir-lang/noir#6599) chore!: remove eddsa from stdlib (noir-lang/noir#6591) chore: Typo in oracles how to (noir-lang/noir#6598) feat(ssa): Loop invariant code motion (noir-lang/noir#6563) fix: remove `compiler_version` from new `Nargo.toml` (noir-lang/noir#6590) feat: Avoid incrementing reference counts in some cases (noir-lang/noir#6568) chore: fix typo in test name (noir-lang/noir#6589) fix: consider prereleases to be compatible with pre-1.0.0 releases (noir-lang/noir#6580) feat: try to inline brillig calls with all constant arguments (noir-lang/noir#6548) fix: correct type when simplifying `derive_pedersen_generators` (noir-lang/noir#6579) feat: Sync from aztec-packages (noir-lang/noir#6576)
2 parents 8de9057 + 3311744 commit f2cef7c

File tree

9 files changed

+156
-34
lines changed

9 files changed

+156
-34
lines changed

.noir-sync-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
594aad21f30614b1733a3ba2b8a2a5f5d7b7e119
1+
4ff308128755c95b4d461bbcb7e3a49f16145585

noir/noir-repo/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

noir/noir-repo/compiler/noirc_driver/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,19 @@ pub struct CompileOptions {
126126
#[arg(long)]
127127
pub skip_underconstrained_check: bool,
128128

129-
/// Setting to decide on an inlining strategy for brillig functions.
129+
/// Setting to decide on an inlining strategy for Brillig functions.
130130
/// A more aggressive inliner should generate larger programs but more optimized
131131
/// A less aggressive inliner should generate smaller programs
132132
#[arg(long, hide = true, allow_hyphen_values = true, default_value_t = i64::MAX)]
133133
pub inliner_aggressiveness: i64,
134+
135+
/// Setting the maximum acceptable increase in Brillig bytecode size due to
136+
/// unrolling small loops. When left empty, any change is accepted as long
137+
/// as it required fewer SSA instructions.
138+
/// A higher value results in fewer jumps but a larger program.
139+
/// A lower value keeps the original program if it was smaller, even if it has more jumps.
140+
#[arg(long, hide = true, allow_hyphen_values = true)]
141+
pub max_bytecode_increase_percent: Option<i32>,
134142
}
135143

136144
pub fn parse_expression_width(input: &str) -> Result<ExpressionWidth, std::io::Error> {
@@ -589,6 +597,7 @@ pub fn compile_no_check(
589597
emit_ssa: if options.emit_ssa { Some(context.package_build_path.clone()) } else { None },
590598
skip_underconstrained_check: options.skip_underconstrained_check,
591599
inliner_aggressiveness: options.inliner_aggressiveness,
600+
max_bytecode_increase_percent: options.max_bytecode_increase_percent,
592601
};
593602

594603
let SsaProgramArtifact { program, debug, warnings, names, brillig_names, error_types, .. } =

noir/noir-repo/compiler/noirc_evaluator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ cfg-if.workspace = true
3333
proptest.workspace = true
3434
similar-asserts.workspace = true
3535
num-traits.workspace = true
36+
test-case.workspace = true
3637

3738
[features]
3839
bn254 = ["noirc_frontend/bn254"]

noir/noir-repo/compiler/noirc_evaluator/src/brillig/mod.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use self::{
1212
},
1313
};
1414
use crate::ssa::{
15-
ir::function::{Function, FunctionId, RuntimeType},
15+
ir::function::{Function, FunctionId},
1616
ssa_gen::Ssa,
1717
};
1818
use fxhash::FxHashMap as HashMap;
@@ -59,17 +59,15 @@ impl std::ops::Index<FunctionId> for Brillig {
5959
}
6060

6161
impl Ssa {
62-
/// Compile to brillig brillig functions and ACIR functions reachable from them
62+
/// Compile Brillig functions and ACIR functions reachable from them
6363
#[tracing::instrument(level = "trace", skip_all)]
6464
pub(crate) fn to_brillig(&self, enable_debug_trace: bool) -> Brillig {
6565
// Collect all the function ids that are reachable from brillig
6666
// That means all the functions marked as brillig and ACIR functions called by them
6767
let brillig_reachable_function_ids = self
6868
.functions
6969
.iter()
70-
.filter_map(|(id, func)| {
71-
matches!(func.runtime(), RuntimeType::Brillig(_)).then_some(*id)
72-
})
70+
.filter_map(|(id, func)| func.runtime().is_brillig().then_some(*id))
7371
.collect::<BTreeSet<_>>();
7472

7573
let mut brillig = Brillig::default();

noir/noir-repo/compiler/noirc_evaluator/src/ssa.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ pub struct SsaEvaluatorOptions {
6767

6868
/// The higher the value, the more inlined brillig functions will be.
6969
pub inliner_aggressiveness: i64,
70+
71+
/// Maximum accepted percentage increase in the Brillig bytecode size after unrolling loops.
72+
/// When `None` the size increase check is skipped altogether and any decrease in the SSA
73+
/// instruction count is accepted.
74+
pub max_bytecode_increase_percent: Option<i32>,
7075
}
7176

7277
pub(crate) struct ArtifactsAndWarnings(Artifacts, Vec<SsaReport>);
@@ -104,7 +109,10 @@ pub(crate) fn optimize_into_acir(
104109
"After `static_assert` and `assert_constant`:",
105110
)?
106111
.run_pass(Ssa::loop_invariant_code_motion, "After Loop Invariant Code Motion:")
107-
.try_run_pass(Ssa::unroll_loops_iteratively, "After Unrolling:")?
112+
.try_run_pass(
113+
|ssa| ssa.unroll_loops_iteratively(options.max_bytecode_increase_percent),
114+
"After Unrolling:",
115+
)?
108116
.run_pass(Ssa::simplify_cfg, "After Simplifying (2nd):")
109117
.run_pass(Ssa::flatten_cfg, "After Flattening:")
110118
.run_pass(Ssa::remove_bit_shifts, "After Removing Bit Shifts:")
@@ -450,11 +458,10 @@ impl SsaBuilder {
450458
}
451459

452460
/// The same as `run_pass` but for passes that may fail
453-
fn try_run_pass(
454-
mut self,
455-
pass: fn(Ssa) -> Result<Ssa, RuntimeError>,
456-
msg: &str,
457-
) -> Result<Self, RuntimeError> {
461+
fn try_run_pass<F>(mut self, pass: F, msg: &str) -> Result<Self, RuntimeError>
462+
where
463+
F: FnOnce(Ssa) -> Result<Ssa, RuntimeError>,
464+
{
458465
self.ssa = time(msg, self.print_codegen_timings, || pass(self.ssa))?;
459466
Ok(self.print(msg))
460467
}

noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/function.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ impl Function {
197197
}
198198
}
199199

200+
impl Clone for Function {
201+
fn clone(&self) -> Self {
202+
Function::clone_with_id(self.id(), self)
203+
}
204+
}
205+
200206
impl std::fmt::Display for RuntimeType {
201207
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202208
match self {

noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs

Lines changed: 116 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
//! When unrolling ACIR code, we remove reference count instructions because they are
2020
//! only used by Brillig bytecode.
2121
use acvm::{acir::AcirField, FieldElement};
22+
use im::HashSet;
2223

2324
use crate::{
25+
brillig::brillig_gen::convert_ssa_function,
2426
errors::RuntimeError,
2527
ssa::{
2628
ir::{
@@ -37,38 +39,60 @@ use crate::{
3739
ssa_gen::Ssa,
3840
},
3941
};
40-
use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
42+
use fxhash::FxHashMap as HashMap;
4143

4244
impl Ssa {
4345
/// Loop unrolling can return errors, since ACIR functions need to be fully unrolled.
4446
/// This meta-pass will keep trying to unroll loops and simplifying the SSA until no more errors are found.
45-
#[tracing::instrument(level = "trace", skip(ssa))]
46-
pub(crate) fn unroll_loops_iteratively(mut ssa: Ssa) -> Result<Ssa, RuntimeError> {
47-
for (_, function) in ssa.functions.iter_mut() {
47+
///
48+
/// The `max_bytecode_incr_pct`, when given, is used to limit the growth of the Brillig bytecode size
49+
/// after unrolling small loops to some percentage of the original loop. For example a value of 150 would
50+
/// mean the new loop can be 150% (ie. 2.5 times) larger than the original loop. It will still contain
51+
/// fewer SSA instructions, but that can still result in more Brillig opcodes.
52+
#[tracing::instrument(level = "trace", skip(self))]
53+
pub(crate) fn unroll_loops_iteratively(
54+
mut self: Ssa,
55+
max_bytecode_increase_percent: Option<i32>,
56+
) -> Result<Ssa, RuntimeError> {
57+
for (_, function) in self.functions.iter_mut() {
58+
// Take a snapshot of the function to compare byte size increase,
59+
// but only if the setting indicates we have to, otherwise skip it.
60+
let orig_func_and_max_incr_pct = max_bytecode_increase_percent
61+
.filter(|_| function.runtime().is_brillig())
62+
.map(|max_incr_pct| (function.clone(), max_incr_pct));
63+
4864
// Try to unroll loops first:
49-
let mut unroll_errors = function.try_unroll_loops();
65+
let (mut has_unrolled, mut unroll_errors) = function.try_unroll_loops();
5066

5167
// Keep unrolling until no more errors are found
5268
while !unroll_errors.is_empty() {
5369
let prev_unroll_err_count = unroll_errors.len();
5470

5571
// Simplify the SSA before retrying
56-
57-
// Do a mem2reg after the last unroll to aid simplify_cfg
58-
function.mem2reg();
59-
function.simplify_function();
60-
// Do another mem2reg after simplify_cfg to aid the next unroll
61-
function.mem2reg();
72+
simplify_between_unrolls(function);
6273

6374
// Unroll again
64-
unroll_errors = function.try_unroll_loops();
75+
let (new_unrolled, new_errors) = function.try_unroll_loops();
76+
unroll_errors = new_errors;
77+
has_unrolled |= new_unrolled;
78+
6579
// If we didn't manage to unroll any more loops, exit
6680
if unroll_errors.len() >= prev_unroll_err_count {
6781
return Err(unroll_errors.swap_remove(0));
6882
}
6983
}
84+
85+
if has_unrolled {
86+
if let Some((orig_function, max_incr_pct)) = orig_func_and_max_incr_pct {
87+
let new_size = brillig_bytecode_size(function);
88+
let orig_size = brillig_bytecode_size(&orig_function);
89+
if !is_new_size_ok(orig_size, new_size, max_incr_pct) {
90+
*function = orig_function;
91+
}
92+
}
93+
}
7094
}
71-
Ok(ssa)
95+
Ok(self)
7296
}
7397
}
7498

@@ -77,7 +101,7 @@ impl Function {
77101
// This can also be true for ACIR, but we have no alternative to unrolling in ACIR.
78102
// Brillig also generally prefers smaller code rather than faster code,
79103
// so we only attempt to unroll small loops, which we decide on a case-by-case basis.
80-
fn try_unroll_loops(&mut self) -> Vec<RuntimeError> {
104+
fn try_unroll_loops(&mut self) -> (bool, Vec<RuntimeError>) {
81105
Loops::find_all(self).unroll_each(self)
82106
}
83107
}
@@ -170,8 +194,10 @@ impl Loops {
170194

171195
/// Unroll all loops within a given function.
172196
/// Any loops which fail to be unrolled (due to using non-constant indices) will be unmodified.
173-
fn unroll_each(mut self, function: &mut Function) -> Vec<RuntimeError> {
197+
/// Returns whether any blocks have been modified
198+
fn unroll_each(mut self, function: &mut Function) -> (bool, Vec<RuntimeError>) {
174199
let mut unroll_errors = vec![];
200+
let mut has_unrolled = false;
175201
while let Some(next_loop) = self.yet_to_unroll.pop() {
176202
if function.runtime().is_brillig() && !next_loop.is_small_loop(function, &self.cfg) {
177203
continue;
@@ -181,21 +207,25 @@ impl Loops {
181207
if next_loop.blocks.iter().any(|block| self.modified_blocks.contains(block)) {
182208
let mut new_loops = Self::find_all(function);
183209
new_loops.failed_to_unroll = self.failed_to_unroll;
184-
return unroll_errors.into_iter().chain(new_loops.unroll_each(function)).collect();
210+
let (new_unrolled, new_errors) = new_loops.unroll_each(function);
211+
return (has_unrolled || new_unrolled, [unroll_errors, new_errors].concat());
185212
}
186213

187214
// Don't try to unroll the loop again if it is known to fail
188215
if !self.failed_to_unroll.contains(&next_loop.header) {
189216
match next_loop.unroll(function, &self.cfg) {
190-
Ok(_) => self.modified_blocks.extend(next_loop.blocks),
217+
Ok(_) => {
218+
has_unrolled = true;
219+
self.modified_blocks.extend(next_loop.blocks);
220+
}
191221
Err(call_stack) => {
192222
self.failed_to_unroll.insert(next_loop.header);
193223
unroll_errors.push(RuntimeError::UnknownLoopBound { call_stack });
194224
}
195225
}
196226
}
197227
}
198-
unroll_errors
228+
(has_unrolled, unroll_errors)
199229
}
200230
}
201231

@@ -947,21 +977,59 @@ impl<'f> LoopIteration<'f> {
947977
}
948978
}
949979

980+
/// Unrolling leaves some duplicate instructions which can potentially be removed.
981+
fn simplify_between_unrolls(function: &mut Function) {
982+
// Do a mem2reg after the last unroll to aid simplify_cfg
983+
function.mem2reg();
984+
function.simplify_function();
985+
// Do another mem2reg after simplify_cfg to aid the next unroll
986+
function.mem2reg();
987+
}
988+
989+
/// Convert the function to Brillig bytecode and return the resulting size.
990+
fn brillig_bytecode_size(function: &Function) -> usize {
991+
// We need to do some SSA passes in order for the conversion to be able to go ahead,
992+
// otherwise we can hit `unreachable!()` instructions in `convert_ssa_instruction`.
993+
// Creating a clone so as not to modify the originals.
994+
let mut temp = function.clone();
995+
996+
// Might as well give it the best chance.
997+
simplify_between_unrolls(&mut temp);
998+
999+
// This is to try to prevent hitting ICE.
1000+
temp.dead_instruction_elimination(false);
1001+
1002+
convert_ssa_function(&temp, false).byte_code.len()
1003+
}
1004+
1005+
/// Decide if the new bytecode size is acceptable, compared to the original.
1006+
///
1007+
/// The maximum increase can be expressed as a negative value if we demand a decrease.
1008+
/// (Values -100 and under mean the new size should be 0).
1009+
fn is_new_size_ok(orig_size: usize, new_size: usize, max_incr_pct: i32) -> bool {
1010+
let max_size_pct = 100i32.saturating_add(max_incr_pct).max(0) as usize;
1011+
let max_size = orig_size.saturating_mul(max_size_pct);
1012+
new_size.saturating_mul(100) <= max_size
1013+
}
1014+
9501015
#[cfg(test)]
9511016
mod tests {
9521017
use acvm::FieldElement;
1018+
use test_case::test_case;
9531019

9541020
use crate::errors::RuntimeError;
9551021
use crate::ssa::{ir::value::ValueId, opt::assert_normalized_ssa_equals, Ssa};
9561022

957-
use super::{BoilerplateStats, Loops};
1023+
use super::{is_new_size_ok, BoilerplateStats, Loops};
9581024

959-
/// Tries to unroll all loops in each SSA function.
1025+
/// Tries to unroll all loops in each SSA function once, calling the `Function` directly,
1026+
/// bypassing the iterative loop done by the SSA which does further optimisations.
1027+
///
9601028
/// If any loop cannot be unrolled, it is left as-is or in a partially unrolled state.
9611029
fn try_unroll_loops(mut ssa: Ssa) -> (Ssa, Vec<RuntimeError>) {
9621030
let mut errors = vec![];
9631031
for function in ssa.functions.values_mut() {
964-
errors.extend(function.try_unroll_loops());
1032+
errors.extend(function.try_unroll_loops().1);
9651033
}
9661034
(ssa, errors)
9671035
}
@@ -1221,9 +1289,26 @@ mod tests {
12211289

12221290
let (ssa, errors) = try_unroll_loops(ssa);
12231291
assert_eq!(errors.len(), 0, "Unroll should have no errors");
1292+
// Check that it's still the original
12241293
assert_normalized_ssa_equals(ssa, parse_ssa().to_string().as_str());
12251294
}
12261295

1296+
#[test]
1297+
fn test_brillig_unroll_iteratively_respects_max_increase() {
1298+
let ssa = brillig_unroll_test_case();
1299+
let ssa = ssa.unroll_loops_iteratively(Some(-90)).unwrap();
1300+
// Check that it's still the original
1301+
assert_normalized_ssa_equals(ssa, brillig_unroll_test_case().to_string().as_str());
1302+
}
1303+
1304+
#[test]
1305+
fn test_brillig_unroll_iteratively_with_large_max_increase() {
1306+
let ssa = brillig_unroll_test_case();
1307+
let ssa = ssa.unroll_loops_iteratively(Some(50)).unwrap();
1308+
// Check that it did the unroll
1309+
assert_eq!(ssa.main().reachable_blocks().len(), 2, "The loop should be unrolled");
1310+
}
1311+
12271312
/// Test that `break` and `continue` stop unrolling without any panic.
12281313
#[test]
12291314
fn test_brillig_unroll_break_and_continue() {
@@ -1377,4 +1462,14 @@ mod tests {
13771462
let loop0 = loops.yet_to_unroll.pop().expect("there should be a loop");
13781463
loop0.boilerplate_stats(function, &loops.cfg).expect("there should be stats")
13791464
}
1465+
1466+
#[test_case(1000, 700, 50, true; "size decreased")]
1467+
#[test_case(1000, 1500, 50, true; "size increased just by the max")]
1468+
#[test_case(1000, 1501, 50, false; "size increased over the max")]
1469+
#[test_case(1000, 700, -50, false; "size decreased but not enough")]
1470+
#[test_case(1000, 250, -50, true; "size decreased over expectations")]
1471+
#[test_case(1000, 250, -1250, false; "demanding more than minus 100 is handled")]
1472+
fn test_is_new_size_ok(old: usize, new: usize, max: i32, ok: bool) {
1473+
assert_eq!(is_new_size_ok(old, new, max), ok);
1474+
}
13801475
}

noir/noir-repo/tooling/nargo_cli/build.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,13 @@ fn test_{test_name}(force_brillig: ForceBrillig, inliner_aggressiveness: Inliner
213213
nargo.arg("--program-dir").arg(test_program_dir);
214214
nargo.arg("{test_command}").arg("--force");
215215
nargo.arg("--inliner-aggressiveness").arg(inliner_aggressiveness.0.to_string());
216+
216217
if force_brillig.0 {{
217218
nargo.arg("--force-brillig");
219+
220+
// Set the maximum increase so that part of the optimization is exercised (it might fail).
221+
nargo.arg("--max-bytecode-increase-percent");
222+
nargo.arg("50");
218223
}}
219224
220225
{test_content}

0 commit comments

Comments
 (0)