Skip to content

Commit d104a00

Browse files
committed
Test stats, count duplicate increments
1 parent 7ffda95 commit d104a00

File tree

1 file changed

+105
-37
lines changed

1 file changed

+105
-37
lines changed

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

+105-37
Original file line numberDiff line numberDiff line change
@@ -41,29 +41,6 @@ use crate::{
4141
};
4242
use fxhash::FxHashMap as HashMap;
4343

44-
/// Number of instructions which are complete loop boilerplate,
45-
/// the ones facilitating jumps and the increment of the loop
46-
/// variable.
47-
///
48-
/// All the instructions in the following example are boilerplate:
49-
/// ```text
50-
/// brillig(inline) fn main f0 {
51-
/// b0(v0: u32):
52-
/// ...
53-
/// jmp b1(u32 0)
54-
/// b1(v1: u32):
55-
/// v5 = lt v1, u32 4
56-
/// jmpif v5 then: b3, else: b2
57-
/// b3():
58-
/// ...
59-
/// v11 = add v1, u32 1
60-
/// jmp b1(v11)
61-
/// b2():
62-
/// ...
63-
/// }
64-
/// ```
65-
const LOOP_BOILERPLATE_COUNT: usize = 5;
66-
6744
impl Ssa {
6845
/// Loop unrolling can return errors, since ACIR functions need to be fully unrolled.
6946
/// This meta-pass will keep trying to unroll loops and simplifying the SSA until no more errors are found.
@@ -276,7 +253,7 @@ impl Loop {
276253
/// b0(v0: u32): // Pre-header
277254
/// ...
278255
/// jmp b1(u32 0) // Lower-bound
279-
/// b1(v1: u32):
256+
/// b1(v1: u32): // Induction variable
280257
/// v5 = lt v1, u32 4
281258
/// jmpif v5 then: b3, else: b2
282259
/// ```
@@ -588,6 +565,25 @@ impl Loop {
588565
.sum()
589566
}
590567

568+
/// Count the number of increments to the induction variable.
569+
/// It should be one, but it can be duplicated.
570+
/// The increment should be in the block where the back-edge was found.
571+
fn count_induction_increments(&self, function: &Function) -> usize {
572+
let back = &function.dfg[self.back_edge_start];
573+
let header = &function.dfg[self.header];
574+
let induction_var = header.parameters()[0];
575+
576+
let mut increments = 0;
577+
for instruction in back.instructions() {
578+
let instruction = &function.dfg[*instruction];
579+
if matches!(instruction, Instruction::Binary(Binary { lhs, operator: BinaryOp::Add, rhs: _ }) if *lhs == induction_var)
580+
{
581+
increments += 1;
582+
}
583+
}
584+
increments
585+
}
586+
591587
/// Decide if this loop is small enough that it can be inlined in a way that the number
592588
/// of unrolled instructions times the number of iterations would result in smaller bytecode
593589
/// than if we keep the loops with their overheads.
@@ -612,34 +608,77 @@ impl Loop {
612608
};
613609
let refs = self.find_pre_header_reference_values(function, cfg);
614610
let (loads, stores) = self.count_loads_and_stores(function, &refs);
611+
let increments = self.count_induction_increments(function);
615612
let all_instructions = self.count_all_instructions(function);
616-
let useful_instructions = all_instructions - loads - stores - LOOP_BOILERPLATE_COUNT;
613+
617614
Some(BoilerplateStats {
618615
iterations: (upper - lower) as usize,
619616
loads,
620617
stores,
618+
increments,
621619
all_instructions,
622-
useful_instructions,
623620
})
624621
}
625622
}
626623

624+
/// All the instructions in the following example are boilerplate:
625+
/// ```text
626+
/// brillig(inline) fn main f0 {
627+
/// b0(v0: u32):
628+
/// ...
629+
/// jmp b1(u32 0)
630+
/// b1(v1: u32):
631+
/// v5 = lt v1, u32 4
632+
/// jmpif v5 then: b3, else: b2
633+
/// b3():
634+
/// ...
635+
/// v11 = add v1, u32 1
636+
/// jmp b1(v11)
637+
/// b2():
638+
/// ...
639+
/// }
640+
/// ```
641+
const LOOP_BOILERPLATE_COUNT: usize = 5;
627642
#[derive(Debug)]
628643
struct BoilerplateStats {
644+
/// Number of iterations in the loop.
629645
iterations: usize,
646+
/// Number of loads pre-header references in the loop.
630647
loads: usize,
648+
/// Number of stores into pre-header references in the loop.
631649
stores: usize,
650+
/// Number of increments to the induction variable (might be duplicated).
651+
increments: usize,
652+
/// Number of instructions in the loop, including boilerplate,
653+
/// but excluding the boilerplate which is outside the loop.
632654
all_instructions: usize,
633-
useful_instructions: usize,
634655
}
635656

636657
impl BoilerplateStats {
658+
/// Instruction count if we leave the loop as-is.
659+
/// It's the instructions in the loop, plus the one to kick it off in the pre-header.
660+
fn baseline_instructions(&self) -> usize {
661+
self.all_instructions + 1
662+
}
663+
664+
/// Estimated number of _useful_ instructions, which is the ones in the loop
665+
/// minus all in-loop boilerplate.
666+
fn useful_instructions(&self) -> usize {
667+
let boilerplate = 3; // Two jumps + plus the comparison with the upper bound
668+
self.all_instructions - self.loads - self.stores - self.increments - boilerplate
669+
}
670+
671+
/// Estimated number of instructions if we unroll the loop.
672+
fn unrolled_instructions(&self) -> usize {
673+
self.useful_instructions() * self.iterations
674+
}
675+
637676
/// A small loop is where if we unroll it into the pre-header then considering the
638677
/// number of iterations we still end up with a smaller bytecode than if we leave
639678
/// the blocks in tact with all the boilerplate involved in jumping, and the extra
640679
/// reference access instructions.
641680
fn is_small(&self) -> bool {
642-
self.useful_instructions * self.iterations < self.all_instructions
681+
self.unrolled_instructions() < self.baseline_instructions()
643682
}
644683
}
645684

@@ -862,7 +901,7 @@ impl<'f> LoopIteration<'f> {
862901
// instances of the induction variable or any values that were changed as a result
863902
// of the new induction variable value.
864903
for instruction in instructions {
865-
if self.skip_ref_counts && self.is_ref_count(instruction) {
904+
if self.skip_ref_counts && self.is_refcount(instruction) {
866905
continue;
867906
}
868907
self.inserter.push_instruction(instruction, self.insert_block);
@@ -878,7 +917,8 @@ impl<'f> LoopIteration<'f> {
878917
self.inserter.function.dfg.set_block_terminator(self.insert_block, terminator);
879918
}
880919

881-
fn is_ref_count(&self, instruction: InstructionId) -> bool {
920+
/// Is the instruction an `Rc`?
921+
fn is_refcount(&self, instruction: InstructionId) -> bool {
882922
matches!(
883923
self.dfg()[instruction],
884924
Instruction::IncrementRc { .. } | Instruction::DecrementRc { .. }
@@ -900,7 +940,7 @@ mod tests {
900940

901941
use crate::ssa::{ir::value::ValueId, opt::assert_normalized_ssa_equals, Ssa};
902942

903-
use super::Loops;
943+
use super::{BoilerplateStats, Loop, Loops};
904944

905945
#[test]
906946
fn unroll_nested_loops() {
@@ -1037,15 +1077,34 @@ mod tests {
10371077
}
10381078

10391079
#[test]
1040-
fn test_is_small_loop() {
1080+
fn test_boilerplate_stats() {
10411081
let ssa = brillig_unroll_test_case();
1042-
let function = ssa.main();
1043-
let mut loops = Loops::find_all(function);
1044-
let loop0 = loops.yet_to_unroll.pop().unwrap();
1082+
let stats = loop0_stats(&ssa);
1083+
assert_eq!(stats.iterations, 4);
1084+
assert_eq!(stats.all_instructions, 2 + 5); // Instructions in b1 and b3
1085+
assert_eq!(stats.increments, 1);
1086+
assert_eq!(stats.loads, 1);
1087+
assert_eq!(stats.stores, 1);
1088+
assert_eq!(stats.useful_instructions(), 1); // Adding to sum
1089+
assert_eq!(stats.baseline_instructions(), 8);
1090+
assert!(stats.is_small());
1091+
}
10451092

1046-
assert!(loop0.is_small_loop(function, &loops.cfg));
1093+
#[test]
1094+
fn test_boilerplate_stats_6470() {
1095+
let ssa = brillig_unroll_test_case_6470(3);
1096+
let stats = loop0_stats(&ssa);
1097+
assert_eq!(stats.iterations, 3);
1098+
assert_eq!(stats.all_instructions, 2 + 8); // Instructions in b1 and b3
1099+
assert_eq!(stats.increments, 2);
1100+
assert_eq!(stats.loads, 1);
1101+
assert_eq!(stats.stores, 1);
1102+
assert_eq!(stats.useful_instructions(), 3); // array get, add, array set
1103+
assert_eq!(stats.baseline_instructions(), 11);
1104+
assert!(stats.is_small());
10471105
}
10481106

1107+
/// Test that we can unroll a small loop.
10491108
#[test]
10501109
fn test_brillig_unroll_small_loop() {
10511110
let ssa = brillig_unroll_test_case();
@@ -1084,6 +1143,7 @@ mod tests {
10841143
assert_normalized_ssa_equals(ssa, expected);
10851144
}
10861145

1146+
/// Test that we can unroll the loop in the ticket if we don't have too many iterations.
10871147
#[test]
10881148
fn test_brillig_unroll_6470_small() {
10891149
// Few enough iterations so that we can perform the unroll.
@@ -1125,6 +1185,7 @@ mod tests {
11251185
assert_normalized_ssa_equals(ssa, expected);
11261186
}
11271187

1188+
/// Test that with more iterations it's not unrolled.
11281189
#[test]
11291190
fn test_brillig_unroll_6470_large() {
11301191
// More iterations than it can unroll
@@ -1218,7 +1279,7 @@ mod tests {
12181279
v10 = array_get v0, index v1 -> u64
12191280
v12 = add v10, u64 1
12201281
v13 = array_set v9, index v1, value v12
1221-
v15 = add v1, u32 1
1282+
v15 = add v1, u32 1 // duplicate unused increment
12221283
store v13 at v4
12231284
v16 = add v1, u32 1
12241285
jmp b1(v16)
@@ -1231,4 +1292,11 @@ mod tests {
12311292
);
12321293
Ssa::from_str(&src).unwrap()
12331294
}
1295+
1296+
fn loop0_stats(ssa: &Ssa) -> BoilerplateStats {
1297+
let function = ssa.main();
1298+
let mut loops = Loops::find_all(function);
1299+
let loop0 = loops.yet_to_unroll.pop().expect("there should be a loop");
1300+
loop0.boilerplate_stats(function, &loops.cfg).expect("there should be stats")
1301+
}
12341302
}

0 commit comments

Comments
 (0)