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, or one
203/// value with another scaled beside it.
204fn part(scev: &mut Scev<'_>, outer: &[LoopId], inv: Invariant) -> bool {
205    let rest = match inv.on() {
206        // Two values in the expression is two values that have to be straight lines, because the
207        // access function is the sum of them and a sum is only as straight as both its sides.
208        // The address of a global is the same number on every iteration of every loop there is,
209        // so it is a straight line and there is nothing to ask about.
210        Some((on, rest)) => {
211            if !on.value().is_none_or(|on| straight(scev, outer, on)) {
212                return false;
213            }
214            rest
215        }
216        None => inv.plain().expect("an invariant not measured from a value is a plain one"),
217    };
218    match rest.value {
219        None => true,
220        Some(value) => straight(scev, outer, value),
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use rucc_base::Interner;
227    use rucc_ir::{
228        Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Opcode, Restrict, Signature, Type,
229        Value,
230    };
231
232    use super::{ALONE, NOT_AFFINE, NOT_PERFECT, Nests, POPULATION, REFERENCE};
233    use crate::stats::Kind;
234    use crate::{Fuel, Pass, Stats};
235
236    /// Runs the survey over the function as it stands.
237    fn survey(func: &mut Func) -> Stats {
238        Nests.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
239    }
240
241    /// An access that says as little about itself as one may.
242    fn plain() -> MemInfo {
243        MemInfo {
244            size: 0,
245            align: 4,
246            order: MemOrder::NotAtomic,
247            tbaa: None,
248            restrict: Restrict::NONE,
249        }
250    }
251
252    /// A counted loop with a body for the caller to fill and a block for it to leave to.
253    struct Counted {
254        head: Block,
255        body: Block,
256        out: Block,
257        counter: Value,
258    }
259
260    /// Opens a loop, entered from `into`.
261    ///
262    /// ```text
263    /// into:      jump head(0)
264    /// head(i):   t = i < limit; br t -> body(i), out
265    /// ```
266    ///
267    /// The counter is handed back because a body that indexes with it is the whole of what this
268    /// pass asks about. Closing the loop is [`close`], and it is separate so that the caller can
269    /// put another loop in the body first.
270    fn counted(func: &mut Func, into: Block, limit: i128) -> Counted {
271        let head = func.create_block();
272        let body = func.create_block();
273        let out = func.create_block();
274        let i = func.append_param(head, Type::int(32));
275        let carried = func.append_param(body, Type::int(32));
276
277        let mut build = Builder::new(func, into);
278        let zero = build.iconst(Type::int(32), 0);
279        build.jump(head, &[zero]);
280
281        let mut build = Builder::new(func, head);
282        let stop = build.iconst(Type::int(32), limit);
283        let test = build.icmp(IntPred::Slt, i, stop);
284        build.br_if(test, body, &[i], out, &[]);
285
286        Counted { head, body, out, counter: carried }
287    }
288
289    /// Closes a loop, with `at` as the block the counter is moved along in.
290    ///
291    /// That is the body for a loop with nothing inside it, and the block the inner loop leaves to
292    /// for a loop with one inside it, which is what makes the nest have nothing between its levels.
293    fn close(func: &mut Func, it: &Counted, at: Block) {
294        let mut build = Builder::new(func, at);
295        let one = build.iconst(Type::int(32), 1);
296        let next = build.binary(Opcode::Add, it.counter, one, Flags::NSW);
297        build.jump(it.head, &[next]);
298    }
299
300    /// A function taking one pointer, with an entry block for a loop to go in.
301    fn shell(names: &mut Interner) -> (Func, Block, Value) {
302        let signature = Signature::new().with_params(&[Type::PTR]);
303        let mut func = Func::new(names.intern("f"), signature);
304        let entry = func.create_block();
305        let base = func.append_param(entry, Type::PTR);
306        (func, entry, base)
307    }
308
309    #[test]
310    fn a_loop_with_nothing_inside_it_is_not_a_nest() {
311        let mut names = Interner::new();
312        let (mut func, entry, _) = shell(&mut names);
313        let it = counted(&mut func, entry, 8);
314        close(&mut func, &it, it.body);
315        Builder::new(&mut func, it.out).ret(&[]);
316
317        let stats = survey(&mut func);
318        assert_eq!(stats.count(Kind::Note, ALONE), 1);
319        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
320        assert!(!stats.changed(), "the survey rewrites nothing");
321    }
322
323    #[test]
324    fn two_loops_walking_a_row_at_a_time_are_the_population() {
325        let mut names = Interner::new();
326        let (mut func, entry, base) = shell(&mut names);
327        let outer = counted(&mut func, entry, 4);
328
329        // row = base + i * 256, worked out once per turn of the outer loop, which is the shape a
330        // two dimensional array walk arrives in.
331        let mut build = Builder::new(&mut func, outer.body);
332        let wide = build.unary(Opcode::SExt, outer.counter, Type::int(64));
333        let stride = build.iconst(Type::int(64), 256);
334        let along = build.binary(Opcode::Mul, wide, stride, Flags::NSW);
335        let row = build.binary(Opcode::PtrAdd, base, along, Flags::NONE);
336
337        let inner = counted(&mut func, outer.body, 3);
338        // row[j] = j.
339        let mut build = Builder::new(&mut func, inner.body);
340        let step = build.unary(Opcode::SExt, inner.counter, Type::int(64));
341        let four = build.iconst(Type::int(64), 4);
342        let by = build.binary(Opcode::Mul, step, four, Flags::NSW);
343        let addr = build.binary(Opcode::PtrAdd, row, by, Flags::NONE);
344        build.store(inner.counter, addr, plain(), Flags::NONE);
345        close(&mut func, &inner, inner.body);
346        close(&mut func, &outer, inner.out);
347        Builder::new(&mut func, outer.out).ret(&[]);
348
349        let stats = survey(&mut func);
350        assert_eq!(stats.count(Kind::Note, POPULATION), 1);
351        assert_eq!(stats.count(Kind::Note, NOT_AFFINE), 0);
352        assert_eq!(stats.count(Kind::Note, REFERENCE), 1);
353    }
354
355    /// The case this pass exists to count, and the day the analysis grew.
356    ///
357    /// `base[i + j]` is affine in both counters by any account of what affine means, and this used
358    /// to come out on the wrong side of the line. Widening the sum to pointer width goes through
359    /// `Scev`'s extension, which took a chrec only when what it starts at was a plain number, and
360    /// what this one starts at is the outer counter. The extension now takes one of a value as
361    /// well, describing rather than naming the widened value, so the sum widens to
362    /// `{sext(i), +, 1}` and both ends of it are straight lines in the loops outside. The test is
363    /// kept the way round it is now because the number the survey reports is a number about rucc's
364    /// analysis and not only about the corpus, and it should move again if the analysis moves.
365    #[test]
366    fn an_address_added_from_both_counters_is_one_this_compiler_can_describe() {
367        let mut names = Interner::new();
368        let (mut func, entry, base) = shell(&mut names);
369        let outer = counted(&mut func, entry, 4);
370        let inner = counted(&mut func, outer.body, 3);
371
372        let mut build = Builder::new(&mut func, inner.body);
373        let sum = build.binary(Opcode::Add, outer.counter, inner.counter, Flags::NSW);
374        let wide = build.unary(Opcode::SExt, sum, Type::int(64));
375        let addr = build.binary(Opcode::PtrAdd, base, wide, Flags::NONE);
376        build.store(outer.counter, addr, plain(), Flags::NONE);
377        close(&mut func, &inner, inner.body);
378        close(&mut func, &outer, inner.out);
379        Builder::new(&mut func, outer.out).ret(&[]);
380
381        let stats = survey(&mut func);
382        assert_eq!(stats.count(Kind::Note, NOT_AFFINE), 0);
383        assert_eq!(stats.count(Kind::Note, POPULATION), 1);
384    }
385
386    #[test]
387    fn a_write_between_the_two_loops_stops_it_being_a_nest() {
388        let mut names = Interner::new();
389        let (mut func, entry, base) = shell(&mut names);
390        let outer = counted(&mut func, entry, 4);
391        let inner = counted(&mut func, outer.body, 3);
392
393        Builder::new(&mut func, inner.body).store(inner.counter, base, plain(), Flags::NONE);
394        close(&mut func, &inner, inner.body);
395        // The write the outer loop does itself, which is the statement beside the inner loop.
396        Builder::new(&mut func, inner.out).store(outer.counter, base, plain(), Flags::NONE);
397        close(&mut func, &outer, inner.out);
398        Builder::new(&mut func, outer.out).ret(&[]);
399
400        let stats = survey(&mut func);
401        assert_eq!(stats.count(Kind::Note, NOT_PERFECT), 1);
402        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
403    }
404
405    #[test]
406    fn an_address_that_came_out_of_memory_is_not_a_straight_line() {
407        let mut names = Interner::new();
408        let (mut func, entry, base) = shell(&mut names);
409        let outer = counted(&mut func, entry, 4);
410        let inner = counted(&mut func, outer.body, 3);
411
412        // p = *base; *p = j, which is the list walk no subscript test describes.
413        let mut build = Builder::new(&mut func, inner.body);
414        let addr = build.load(Type::PTR, base, plain(), Flags::NONE);
415        build.store(inner.counter, addr, plain(), Flags::NONE);
416        close(&mut func, &inner, inner.body);
417        close(&mut func, &outer, inner.out);
418        Builder::new(&mut func, outer.out).ret(&[]);
419
420        let stats = survey(&mut func);
421        assert_eq!(stats.count(Kind::Note, NOT_AFFINE), 1);
422        assert_eq!(stats.count(Kind::Note, POPULATION), 0);
423        assert_eq!(stats.count(Kind::Note, REFERENCE), 2, "the load and the write both count");
424    }
425}