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