@@ -41,29 +41,6 @@ use crate::{
41
41
} ;
42
42
use fxhash:: FxHashMap as HashMap ;
43
43
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
-
67
44
impl Ssa {
68
45
/// Loop unrolling can return errors, since ACIR functions need to be fully unrolled.
69
46
/// 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 {
276
253
/// b0(v0: u32): // Pre-header
277
254
/// ...
278
255
/// jmp b1(u32 0) // Lower-bound
279
- /// b1(v1: u32):
256
+ /// b1(v1: u32): // Induction variable
280
257
/// v5 = lt v1, u32 4
281
258
/// jmpif v5 then: b3, else: b2
282
259
/// ```
@@ -588,6 +565,25 @@ impl Loop {
588
565
. sum ( )
589
566
}
590
567
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
+
591
587
/// Decide if this loop is small enough that it can be inlined in a way that the number
592
588
/// of unrolled instructions times the number of iterations would result in smaller bytecode
593
589
/// than if we keep the loops with their overheads.
@@ -612,34 +608,77 @@ impl Loop {
612
608
} ;
613
609
let refs = self . find_pre_header_reference_values ( function, cfg) ;
614
610
let ( loads, stores) = self . count_loads_and_stores ( function, & refs) ;
611
+ let increments = self . count_induction_increments ( function) ;
615
612
let all_instructions = self . count_all_instructions ( function) ;
616
- let useful_instructions = all_instructions - loads - stores - LOOP_BOILERPLATE_COUNT ;
613
+
617
614
Some ( BoilerplateStats {
618
615
iterations : ( upper - lower) as usize ,
619
616
loads,
620
617
stores,
618
+ increments,
621
619
all_instructions,
622
- useful_instructions,
623
620
} )
624
621
}
625
622
}
626
623
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 ;
627
642
#[ derive( Debug ) ]
628
643
struct BoilerplateStats {
644
+ /// Number of iterations in the loop.
629
645
iterations : usize ,
646
+ /// Number of loads pre-header references in the loop.
630
647
loads : usize ,
648
+ /// Number of stores into pre-header references in the loop.
631
649
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.
632
654
all_instructions : usize ,
633
- useful_instructions : usize ,
634
655
}
635
656
636
657
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
+
637
676
/// A small loop is where if we unroll it into the pre-header then considering the
638
677
/// number of iterations we still end up with a smaller bytecode than if we leave
639
678
/// the blocks in tact with all the boilerplate involved in jumping, and the extra
640
679
/// reference access instructions.
641
680
fn is_small ( & self ) -> bool {
642
- self . useful_instructions * self . iterations < self . all_instructions
681
+ self . unrolled_instructions ( ) < self . baseline_instructions ( )
643
682
}
644
683
}
645
684
@@ -862,7 +901,7 @@ impl<'f> LoopIteration<'f> {
862
901
// instances of the induction variable or any values that were changed as a result
863
902
// of the new induction variable value.
864
903
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) {
866
905
continue ;
867
906
}
868
907
self . inserter . push_instruction ( instruction, self . insert_block ) ;
@@ -878,7 +917,8 @@ impl<'f> LoopIteration<'f> {
878
917
self . inserter . function . dfg . set_block_terminator ( self . insert_block , terminator) ;
879
918
}
880
919
881
- fn is_ref_count ( & self , instruction : InstructionId ) -> bool {
920
+ /// Is the instruction an `Rc`?
921
+ fn is_refcount ( & self , instruction : InstructionId ) -> bool {
882
922
matches ! (
883
923
self . dfg( ) [ instruction] ,
884
924
Instruction :: IncrementRc { .. } | Instruction :: DecrementRc { .. }
@@ -900,7 +940,7 @@ mod tests {
900
940
901
941
use crate :: ssa:: { ir:: value:: ValueId , opt:: assert_normalized_ssa_equals, Ssa } ;
902
942
903
- use super :: Loops ;
943
+ use super :: { BoilerplateStats , Loop , Loops } ;
904
944
905
945
#[ test]
906
946
fn unroll_nested_loops ( ) {
@@ -1037,15 +1077,34 @@ mod tests {
1037
1077
}
1038
1078
1039
1079
#[ test]
1040
- fn test_is_small_loop ( ) {
1080
+ fn test_boilerplate_stats ( ) {
1041
1081
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
+ }
1045
1092
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( ) ) ;
1047
1105
}
1048
1106
1107
+ /// Test that we can unroll a small loop.
1049
1108
#[ test]
1050
1109
fn test_brillig_unroll_small_loop ( ) {
1051
1110
let ssa = brillig_unroll_test_case ( ) ;
@@ -1084,6 +1143,7 @@ mod tests {
1084
1143
assert_normalized_ssa_equals ( ssa, expected) ;
1085
1144
}
1086
1145
1146
+ /// Test that we can unroll the loop in the ticket if we don't have too many iterations.
1087
1147
#[ test]
1088
1148
fn test_brillig_unroll_6470_small ( ) {
1089
1149
// Few enough iterations so that we can perform the unroll.
@@ -1125,6 +1185,7 @@ mod tests {
1125
1185
assert_normalized_ssa_equals ( ssa, expected) ;
1126
1186
}
1127
1187
1188
+ /// Test that with more iterations it's not unrolled.
1128
1189
#[ test]
1129
1190
fn test_brillig_unroll_6470_large ( ) {
1130
1191
// More iterations than it can unroll
@@ -1218,7 +1279,7 @@ mod tests {
1218
1279
v10 = array_get v0, index v1 -> u64
1219
1280
v12 = add v10, u64 1
1220
1281
v13 = array_set v9, index v1, value v12
1221
- v15 = add v1, u32 1
1282
+ v15 = add v1, u32 1 // duplicate unused increment
1222
1283
store v13 at v4
1223
1284
v16 = add v1, u32 1
1224
1285
jmp b1(v16)
@@ -1231,4 +1292,11 @@ mod tests {
1231
1292
) ;
1232
1293
Ssa :: from_str ( & src) . unwrap ( )
1233
1294
}
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
+ }
1234
1302
}
0 commit comments