Skip to main content

rucc_opt/
nests.rs

1//! Counts the loop nests documents 30 and 31 are gated on, and changes nothing at all.
2//!
3//! Design: `spec/optimizer/30-loop-restructuring.md` section 30.8 and
4//! `spec/optimizer/31-dependence-analysis.md` section 31.8.
5//!
6//! Both of those documents decline to build anything in M4, and both of them decline on the same
7//! number. Section 30.8 asks for the fraction of the corpus spent in loops that are perfectly
8//! nested at depth two or more with affine subscripts, and says it is the one measurement that
9//! would overturn the decision. Section 31.8 says everything in documents 30, 31 and 32 is
10//! downstream of it and to collect it first. This pass is the collecting.
11//!
12//! It is also section 31.3's instrumentation-first approach applied one step earlier than that
13//! section applies it. Document 31 wants the subscript tests instrumented so that where to add
14//! power is decided by counts rather than by intuition. Before any of those tests exist there is a
15//! prior count, which is how many nests there are for them to run on at all, and the pass that
16//! takes it is a fiftieth of the size of the ones the answer would authorize.
17//!
18//! # It is not in any pipeline
19//!
20//! Nothing here is worth a walk over every function of every build, so no optimization level names
21//! it. What reaches it is `-fenable-nests`, which section 41.6 already makes pull a pass into the
22//! pipeline that the level did not choose, together with `-fopt-info-all` to hear what it found.
23//! Surveying the corpus is then one run of the corpus with two flags on, which is section 30.8's
24//! claim that the number costs one instrumented run to obtain.
25//!
26//! # What it counts
27//!
28//! One remark per nest, and the nests are counted from the outside in. Starting at a loop with
29//! nothing around it, the chain goes down for as long as the loop it is at has exactly one loop
30//! inside it and nothing between the two of them, and it stops at a loop with no loop inside it.
31//! A chain that stops anywhere else is not perfectly nested and is counted as that.
32//!
33//! **Nothing between the two of them** is read as no instruction that touches memory in the blocks
34//! of the outer loop that are not blocks of the inner one. That is narrower than perfect nesting
35//! strictly means, since scalar arithmetic between the loops breaks the perfect nesting too, and it
36//! is the right width for what the number is for. A subscript computation hoisted out of the inner
37//! loop is arithmetic between the loops that every transformation in document 30 sinks back before
38//! it does anything else, so counting those nests out would undercount the population that the
39//! transformations serve. A store between the loops is a statement, and that is the shape those
40//! transformations genuinely cannot have.
41//!
42//! **A straight line in the counters** is what document 31.1 calls an affine access function. The
43//! address of every read and write in the innermost loop is asked of `crate::scev` once per loop of
44//! the chain, and it has to come back as something that either does not move or moves by a fixed
45//! step. Anything else, and a call is the common anything else, means the equation document 31.1
46//! states is not a linear one and none of the tests in section 31.2 apply.
47//!
48//! The count of references is reported too, one remark each, because section 31.7's cost is
49//! quadratic in it: a nest with fifty references has 1,225 subscript pairs, and how large that
50//! number gets on real code is the second thing worth knowing before writing the tests.
51//!
52//! # What it does not do
53//!
54//! It does not weight anything by run time, and section 30.8 asks for a fraction of run time rather
55//! than a count of nests. Static counts are the half of the answer a compiler can give on its own.
56//! The other half is which of those nests the corpus actually spends its time in, which is a
57//! profile, and it belongs to the corpus rather than here.
58
59use rucc_ir::{Func, Inst, Opcode, Value};
60
61use crate::loops::{LoopId, Loops};
62use crate::scev::{Evolution, Invariant, Scev};
63use crate::{Analyses, Fuel, Pass, Preserved, Stats};
64
65const POPULATION: &str =
66    "loop nest two or more deep, perfectly nested, every address in it a straight line";
67const NOT_AFFINE: &str =
68    "loop nest two or more deep, perfectly nested, an address in it is not a straight line";
69const NOT_PERFECT: &str = "loop nest, but not perfectly nested, something sits between the loops";
70const ALONE: &str = "loop with no loop inside it";
71const REFERENCE: &str = "read or write in the innermost loop of a perfect nest";
72
73/// The survey section 30.8 asks for.
74#[derive(Debug)]
75pub struct Nests;
76
77impl Pass for Nests {
78    fn name(&self) -> &'static str {
79        "nests"
80    }
81
82    fn describe(&self) -> &'static str {
83        "counts the loop nests, and changes nothing"
84    }
85
86    fn preserves(&self) -> Preserved {
87        // It writes nothing, so everything worked out about the function is still true.
88        Preserved::ALL
89    }
90
91    fn run(&self, func: &mut Func, an: &mut Analyses, _fuel: &mut Fuel) -> Stats {
92        let mut stats = Stats::new();
93        if func.entry().is_none() {
94            return stats;
95        }
96        let cfg = an.cfg(func).clone();
97        let loops = an.loops(func).clone();
98        let mut scev = Scev::new(func, &cfg, &loops);
99        for id in loops.all() {
100            if loops.parent(id).is_some() {
101                continue;
102            }
103            match chain(func, &loops, id) {
104                Chain::Broken => stats.note(NOT_PERFECT),
105                Chain::Perfect(nest) => report(func, &loops, &mut scev, &nest, &mut stats),
106            }
107        }
108        stats
109    }
110}
111
112/// How far the loops go down before something stops them being one nest.
113enum Chain {
114    /// The loops from the outside in, ending at one with no loop inside it.
115    Perfect(Vec<LoopId>),
116    /// A loop that holds more than one loop, or holds one with a statement beside it.
117    Broken,
118}
119
120/// The chain of loops starting at this one, going in for as long as it stays a nest.
121fn chain(func: &Func, loops: &Loops, outer: LoopId) -> Chain {
122    let mut nest = vec![outer];
123    let mut at = outer;
124    loop {
125        let inside = loops.children(at);
126        let [only] = inside else {
127            return match inside.is_empty() {
128                true => Chain::Perfect(nest),
129                false => Chain::Broken,
130            };
131        };
132        if between(func, loops, at, *only) {
133            return Chain::Broken;
134        }
135        nest.push(*only);
136        at = *only;
137    }
138}
139
140/// Whether anything touching memory sits in the outer loop and not in the inner one.
141fn between(func: &Func, loops: &Loops, outer: LoopId, inner: LoopId) -> bool {
142    loops
143        .blocks(outer)
144        .iter()
145        .filter(|&&block| !loops.contains(inner, block))
146        .flat_map(|&block| func.insts(block))
147        .any(|inst| func[inst].opcode.touches_memory())
148}
149
150/// Says which kind of nest this one is, and counts what its innermost loop reads and writes.
151fn report(func: &Func, loops: &Loops, scev: &mut Scev<'_>, nest: &[LoopId], stats: &mut Stats) {
152    let Some(&innermost) = nest.last() else { return };
153    if nest.len() < 2 {
154        stats.note(ALONE);
155        return;
156    }
157    let mut affine = true;
158    let touching: Vec<Inst> = loops
159        .blocks(innermost)
160        .iter()
161        .flat_map(|&block| func.insts(block))
162        .filter(|&inst| func[inst].opcode.touches_memory())
163        .collect();
164    for inst in touching {
165        stats.note(REFERENCE);
166        affine &= match address(func, inst) {
167            // A call is the usual one here. It touches memory at an address nothing named, so
168            // there is no access function to be affine and document 31.1's equation has no terms.
169            None => false,
170            Some(addr) => straight(scev, nest, addr),
171        };
172    }
173    stats.note(if affine { POPULATION } else { NOT_AFFINE });
174}
175
176/// The address a read or a write names, when it names one.
177fn address(func: &Func, inst: Inst) -> Option<Value> {
178    let data = func[inst];
179    let args = &func[data.args];
180    match data.opcode {
181        Opcode::Load => args.first().copied(),
182        Opcode::Store => args.get(1).copied(),
183        _ => None,
184    }
185}
186
187/// Whether that value is a straight line in the counters of this nest.
188///
189/// Asked innermost first, because that is how a nest of them is built. A value that moves by a
190/// fixed step in the innermost loop is a straight line there if what it starts at and what it
191/// steps by are themselves straight lines in the loops outside, which is the same recursion
192/// document 31.1's access function is written by.
193fn straight(scev: &mut Scev<'_>, nest: &[LoopId], value: Value) -> bool {
194    let Some((&innermost, outer)) = nest.split_last() else { return true };
195    match scev.evolution(innermost, value) {
196        Evolution::Unknown => false,
197        Evolution::Invariant(inv) => part(scev, outer, inv),
198        Evolution::Affine(chrec) => part(scev, outer, chrec.base) && part(scev, outer, chrec.step),
199    }
200}
201
202/// The same question about one end of a chrec, which is a number or a value and a scale.
203fn part(scev: &mut Scev<'_>, outer: &[LoopId], inv: Invariant) -> bool {
204    match inv.value {
205        None => true,
206        Some(value) => straight(scev, outer, value),
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use rucc_base::Interner;
213    use rucc_ir::{
214        Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
215        Value,
216    };
217
218    use super::{ALONE, NOT_AFFINE, NOT_PERFECT, Nests, POPULATION, REFERENCE};
219    use crate::stats::Kind;
220    use crate::{Fuel, Pass, Stats};
221
222    /// Runs the survey over the function as it stands.
223    fn survey(func: &mut Func) -> Stats {
224        Nests.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
225    }
226
227    /// An access that says as little about itself as one may.
228    fn plain() -> MemInfo {
229        MemInfo {
230            size: 0,
231            align: 4,
232            order: MemOrder::NotAtomic,
233            tbaa: None,
234            restrict: Restrict::NONE,
235        }
236    }
237
238    /// A counted loop with a body for the caller to fill and a block for it to leave to.
239    struct Counted {
240        head: Block,
241        body: Block,
242        out: Block,
243        counter: Value,
244    }
245
246    /// Opens a loop, entered from `into`.
247    ///
248    /// ```text
249    /// into:      jump head(0)
250    /// head(i):   t = i < limit; br t -> body(i), out
251    /// ```
252    ///
253    /// The counter is handed back because a body that indexes with it is the whole of what this
254    /// pass asks about. Closing the loop is [`close`], and it is separate so that the caller can
255    /// put another loop in the body first.
256    fn counted(func: &mut Func, into: Block, limit: i128) -> Counted {
257        let head = func.create_block();
258        let body = func.create_block();
259        let out = func.create_block();
260        let i = func.append_param(head, Type::int(32));
261        let carried = func.append_param(body, Type::int(32));
262
263        let mut build = Builder::new(func, into);
264        let zero = build.iconst(Type::int(32), 0);
265        build.jump(head, &[zero]);
266
267        let mut build = Builder::new(func, head);
268        let stop = build.iconst(Type::int(32), limit);
269        let test = build.icmp(IntPred::Slt, i, stop);
270        build.br_if(test, body, &[i], out, &[]);
271
272        Counted { head, body, out, counter: carried }
273    }
274
275    /// Closes a loop, with `at` as the block the counter is moved along in.
276    ///
277    /// That is the body for a loop with nothing inside it, and the block the inner loop leaves to
278    /// for a loop with one inside it, which is what makes the nest have nothing between its levels.
279    fn close(func: &mut Func, it: &Counted, at: Block) {
280        let mut build = Builder::new(func, at);
281        let one = build.iconst(Type::int(32), 1);
282        let next = build.binary(Opcode::Add, it.counter, one, Flags::NSW);
283        build.jump(it.head, &[next]);
284    }
285
286    /// A function taking one pointer, with an entry block for a loop to go in.
287    fn shell(names: &mut Interner) -> (Func, Block, Value) {
288        let signature = Signature::new().with_params(&[Type::PTR]);
289        let mut func = Func::new(names.intern("f"), signature);
290        let entry = func.create_block();
291        let base = func.append_param(entry, Type::PTR);
292        (func, entry, base)
293    }
294
295    #[test]
296    fn a_loop_with_nothing_inside_it_is_not_a_nest() {
297        let mut names = Interner::new();
298        let (mut func, entry, _) = shell(&mut names);
299        let it = counted(&mut func, entry, 8);
300        close(&mut func, &it, it.body);
301        Builder::new(&mut func, it.out).ret(&[]);
302
303        let stats = survey(&mut func);
304        assert_eq!(stats.count(Kind::Note, ALONE), 1);
305        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
306        assert!(!stats.changed(), "the survey rewrites nothing");
307    }
308
309    #[test]
310    fn two_loops_walking_a_row_at_a_time_are_the_population() {
311        let mut names = Interner::new();
312        let (mut func, entry, base) = shell(&mut names);
313        let outer = counted(&mut func, entry, 4);
314
315        // row = base + i * 256, worked out once per turn of the outer loop, which is the shape a
316        // two dimensional array walk arrives in.
317        let mut build = Builder::new(&mut func, outer.body);
318        let wide = build.unary(Opcode::SExt, outer.counter, Type::int(64));
319        let stride = build.iconst(Type::int(64), 256);
320        let along = build.binary(Opcode::Mul, wide, stride, Flags::NSW);
321        let row = build.binary(Opcode::PtrAdd, base, along, Flags::NONE);
322
323        let inner = counted(&mut func, outer.body, 3);
324        // row[j] = j.
325        let mut build = Builder::new(&mut func, inner.body);
326        let step = build.unary(Opcode::SExt, inner.counter, Type::int(64));
327        let four = build.iconst(Type::int(64), 4);
328        let by = build.binary(Opcode::Mul, step, four, Flags::NSW);
329        let addr = build.binary(Opcode::PtrAdd, row, by, Flags::NONE);
330        build.store(inner.counter, addr, plain(), Flags::NONE);
331        close(&mut func, &inner, inner.body);
332        close(&mut func, &outer, inner.out);
333        Builder::new(&mut func, outer.out).ret(&[]);
334
335        let stats = survey(&mut func);
336        assert_eq!(stats.count(Kind::Note, POPULATION), 1);
337        assert_eq!(stats.count(Kind::Note, NOT_AFFINE), 0);
338        assert_eq!(stats.count(Kind::Note, REFERENCE), 1);
339    }
340
341    /// The case this pass exists to count, and it comes out on the wrong side of the line.
342    ///
343    /// `base[i + j]` is affine in both counters by any account of what affine means, and it is not
344    /// one this compiler can say so about. Widening the sum to pointer width goes through
345    /// `Scev`'s extension, which takes a chrec only when what it starts at is a plain number, and
346    /// what this one starts at is the outer counter. The number the survey reports is a number
347    /// about rucc's analysis and not only about the corpus, and that is worth a test of its own so
348    /// that the day the analysis grows the number moves and somebody notices.
349    #[test]
350    fn an_address_added_from_both_counters_is_not_one_this_compiler_can_describe() {
351        let mut names = Interner::new();
352        let (mut func, entry, base) = shell(&mut names);
353        let outer = counted(&mut func, entry, 4);
354        let inner = counted(&mut func, outer.body, 3);
355
356        let mut build = Builder::new(&mut func, inner.body);
357        let sum = build.binary(Opcode::Add, outer.counter, inner.counter, Flags::NSW);
358        let wide = build.unary(Opcode::SExt, sum, Type::int(64));
359        let addr = build.binary(Opcode::PtrAdd, base, wide, Flags::NONE);
360        build.store(outer.counter, addr, plain(), Flags::NONE);
361        close(&mut func, &inner, inner.body);
362        close(&mut func, &outer, inner.out);
363        Builder::new(&mut func, outer.out).ret(&[]);
364
365        let stats = survey(&mut func);
366        assert_eq!(stats.count(Kind::Note, NOT_AFFINE), 1);
367        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
368    }
369
370    #[test]
371    fn a_write_between_the_two_loops_stops_it_being_a_nest() {
372        let mut names = Interner::new();
373        let (mut func, entry, base) = shell(&mut names);
374        let outer = counted(&mut func, entry, 4);
375        let inner = counted(&mut func, outer.body, 3);
376
377        Builder::new(&mut func, inner.body).store(inner.counter, base, plain(), Flags::NONE);
378        close(&mut func, &inner, inner.body);
379        // The write the outer loop does itself, which is the statement beside the inner loop.
380        Builder::new(&mut func, inner.out).store(outer.counter, base, plain(), Flags::NONE);
381        close(&mut func, &outer, inner.out);
382        Builder::new(&mut func, outer.out).ret(&[]);
383
384        let stats = survey(&mut func);
385        assert_eq!(stats.count(Kind::Note, NOT_PERFECT), 1);
386        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
387    }
388
389    #[test]
390    fn an_address_that_came_out_of_memory_is_not_a_straight_line() {
391        let mut names = Interner::new();
392        let (mut func, entry, base) = shell(&mut names);
393        let outer = counted(&mut func, entry, 4);
394        let inner = counted(&mut func, outer.body, 3);
395
396        // p = *base; *p = j, which is the list walk no subscript test describes.
397        let mut build = Builder::new(&mut func, inner.body);
398        let addr = build.load(Type::PTR, base, plain(), Flags::NONE);
399        build.store(inner.counter, addr, plain(), Flags::NONE);
400        close(&mut func, &inner, inner.body);
401        close(&mut func, &outer, inner.out);
402        Builder::new(&mut func, outer.out).ret(&[]);
403
404        let stats = survey(&mut func);
405        assert_eq!(stats.count(Kind::Note, NOT_AFFINE), 1);
406        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
407        assert_eq!(stats.count(Kind::Note, REFERENCE), 2, "the load and the write both count");
408    }
409}