Skip to main content

rucc_opt/
header_copy.rs

1//! Copies a loop's header in front of the loop, so the test ends up at the bottom.
2//!
3//! Design: `spec/optimizer/26-loop-canonicalization.md` section 26.6, with 26.7 for where it sits
4//! and 26.8 for the two ways it goes wrong.
5//!
6//! [`crate::canon`] establishes the four properties every loop pass is allowed to assume and
7//! generates nothing on its own. This is the fifth property and it is the one that changes the
8//! program. A `while (c) { body }` tests at the top, so its header is a join and a branch at once
9//! and the test runs once more than the body does. Copying the header in front of the loop turns
10//! it into `if (c) { do { body } while (c); }`, which evaluates the condition exactly as often and
11//! leaves a loop whose body is a single region and whose exit test is at the bottom where the
12//! induction variable's last value is.
13//!
14//! # What it is really for
15//!
16//! Section 26.6 says the largest single benefit is not the shape. It is that after the copy the
17//! entry test stands in front of the loop where document 10's ranges can be asked about it, and
18//! where the ranges settle it the loop is known to run at least one iteration. That is what turns
19//! a trip count estimate into a bound, what lets hoisting move a computation out without proving
20//! it safe to speculate, and what saves the vectorizer a guard. So the range query is not a
21//! refinement on the copy, it is half of the reason to make it, and it happens here rather than
22//! being left to [`crate::prune`] because prune has already run by the time the loop pipeline
23//! opens.
24//!
25//! # One block, not a chain
26//!
27//! GCC copies as many blocks as its budget allows, walking down from the header while
28//! `should_duplicate_loop_header_p` keeps saying yes. This copies the header and stops. The header
29//! is where the exit test is, so one block is what the do-while form needs, and a chain buys the
30//! cases where the condition is spread over several blocks that nothing has managed to merge. The
31//! bound is the same either way and the second block can be added when the corpus says which
32//! programs want it.
33//!
34//! Copying a header could otherwise feed itself: the block the copy makes the new header of the
35//! loop may test and exit as well, and copying that one exposes a third. Every header this pass
36//! copies and every block it makes a header of are put aside, so each loop is looked at once per
37//! run and the growth is bounded by the loop count rather than by how the branches happen to nest.
38//!
39//! # Why the copy repeats nothing
40//!
41//! The copy runs exactly where the header's first execution used to, so nothing in the program
42//! happens a different number of times. That argument would let a store or a call be copied, and
43//! section 26.8 refuses both anyway, through document 17.1's whitelist, which is
44//! [`Opcode::has_effects`]. The reason to keep the refusal is that the argument above holds for
45//! one block and stops holding the moment the copy is a chain, and a pass whose correctness
46//! depends on a bound somebody may raise later is one that will be wrong later. Refusing here
47//! costs the headers with a load in them, which document 27's hoisting is the pass for.
48//!
49//! # What the copy owes the values
50//!
51//! The header used to dominate the whole loop. After the copy it does not: the body is reached
52//! from the copy as well, so a value the header defined and the body read has two definitions
53//! reaching it and needs a merge. The merge goes where the two paths meet, which is the body, as
54//! one more block parameter carrying the header's value on the back edge and the copy's on the
55//! way in.
56//!
57//! Values the header defines and something outside the loop reads are refused rather than merged.
58//! After [`crate::canon`] there are none, because loop-closed form has already routed them through
59//! the exit, so the case this declines is the one where somebody ran this pass without the
60//! canonicalizer and the answer to that is a missed optimization rather than a second merge
61//! written for a shape the pipeline does not produce.
62//!
63//! # Which level
64//!
65//! `-O1` and above at [`SPEED`]'s budget, which is GCC's twenty. `-Os` at [`SIZE`]'s, which is
66//! section 26.6's five, because the do-while form is slightly smaller in the steady state and the
67//! copy is what it costs. `-Oz` does not run it at all. Two passes rather than one with a knob,
68//! because a pass here is a name a `-f` flag spells and there is nowhere for a level to hand a
69//! pass a number.
70
71use std::collections::{HashMap, HashSet};
72
73use rucc_cost::heuristics;
74use rucc_ir::{
75    Block, BlockCall, Builder, ExtraKind, Func, Inst, InstData, Opcode, Type, Value, ValueList,
76};
77
78use crate::cfg::Cfg;
79use crate::dom::Dominators;
80use crate::loops::{LoopId, Loops};
81use crate::range::query::Ranges;
82use crate::{Analyses, Fuel, Pass, Preserved, Stats, prune, simplify_cfg};
83
84const COPIED: &str = "loop header copied in front of the loop so the test is at the bottom";
85const ENTERED: &str = "entry test removed, the value ranges say the loop runs";
86const SKIPPED: &str = "loop removed, the value ranges say the entry test never holds";
87const UNDECIDED: &str = "entry test kept, the value ranges do not settle whether the loop runs";
88const ALREADY: &str = "loop left as it was, it already tests at the bottom";
89const TOO_BIG: &str = "loop header not copied, it is larger than this level allows";
90const EFFECTS: &str = "loop header not copied, something in it may not be repeated";
91const SHAPE: &str = "loop header not copied, its exit is not a two way branch";
92const ESCAPES: &str = "loop header not copied, a value it defines is read outside the loop";
93const NO_PREHEADER: &str = "loop header not copied, the loop has not been canonicalized";
94const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
95
96/// Section 26.6's transformation, at one budget.
97///
98/// The budget is a field rather than a constant because the two levels that run this want
99/// different ones, and it comes with the name for the same reason: the two instances are two
100/// entries in [`crate::pass::PASSES`] and a pipeline picks one by naming it.
101#[derive(Debug)]
102pub struct HeaderCopy {
103    /// What a `-f` flag spells.
104    name: &'static str,
105    /// How many instructions a header may hold and still be worth copying, which is
106    /// [`heuristics::LOOP_HEADER_INSNS_FOR_SPEED`] or the size one next to it.
107    budget: u32,
108}
109
110/// The instance `-O1`, `-O2` and `-O3` run, at GCC's budget.
111pub static SPEED: HeaderCopy =
112    HeaderCopy { name: "header-copy", budget: heuristics::LOOP_HEADER_INSNS_FOR_SPEED };
113
114/// The instance `-Os` runs, at section 26.6's smaller one.
115pub static SIZE: HeaderCopy =
116    HeaderCopy { name: "header-copy-small", budget: heuristics::LOOP_HEADER_INSNS_FOR_SIZE };
117
118impl Pass for HeaderCopy {
119    fn name(&self) -> &'static str {
120        self.name
121    }
122
123    fn describe(&self) -> &'static str {
124        "copies a loop header in front of the loop, turning a while into a do-while"
125    }
126
127    fn preserves(&self) -> Preserved {
128        // A block appears, two edges become four, and the body grows a parameter the loop carries.
129        Preserved::NONE
130    }
131
132    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
133        let mut stats = Stats::new();
134        if func.entry().is_none() {
135            return stats;
136        }
137        let mut done = HashSet::new();
138        let mut say = true;
139        let mut dry = false;
140        loop {
141            let jobs = self.plan(func, an, &done, &mut stats, say);
142            say = false;
143            if jobs.is_empty() {
144                break;
145            }
146            let mut copies = Vec::with_capacity(jobs.len());
147            for job in &jobs {
148                if !fuel.take() {
149                    stats.missed(NO_FUEL);
150                    dry = true;
151                    break;
152                }
153                done.insert(job.header);
154                done.insert(job.body);
155                copies.push(apply(func, job));
156                stats.optimized(COPIED);
157            }
158            an.clear();
159            if settle(func, an, &copies, &mut stats) {
160                an.clear();
161            }
162            if dry {
163                break;
164            }
165        }
166        if stats.changed() {
167            // Section 6.5 leaves the stranded blocks to whoever stranded them, and a loop whose
168            // entry test the ranges disproved is a loop nothing reaches any more.
169            simplify_cfg::sweep(func, an, &mut stats);
170        }
171        an.clear();
172        stats
173    }
174}
175
176/// One loop to copy the header of, worked out against the function as it stands.
177#[derive(Debug)]
178struct Job {
179    /// The loop, which is what [`independent`] asks about to decide whether two jobs meet.
180    id: LoopId,
181    /// The block holding the exit test.
182    header: Block,
183    /// The one block outside the loop the header is reached from.
184    entry: Block,
185    /// The header's successor inside the loop, which the copy makes the new header.
186    ///
187    /// The other one, which is where the loop leaves from, is not recorded. The copy branches to
188    /// both by copying the header's own terminator, and nothing after that has a question to ask
189    /// about the one that goes out.
190    body: Block,
191    /// The values the header defines and the rest of the loop reads, which need a merge at
192    /// [`Job::body`] once there are two ways to get there.
193    carried: Vec<Value>,
194}
195
196/// One loop the cheap checks accepted, waiting on the walk that says what it carries.
197#[derive(Debug)]
198struct Candidate {
199    /// The loop.
200    id: LoopId,
201    /// The block holding the exit test.
202    header: Block,
203    /// The one block outside the loop the header is reached from.
204    entry: Block,
205    /// The header's successor inside the loop.
206    body: Block,
207    /// Everything the header defines, which [`carried`] sorts into what the loop reads and what
208    /// nothing does.
209    defined: Vec<Value>,
210}
211
212impl HeaderCopy {
213    /// Every loop worth copying the header of that can be copied without looking again.
214    ///
215    /// A round rather than one at a time. A copy changes the shape of the loop it is made for, so
216    /// the forest this was read out of is wrong about that loop afterwards, and the answer used to
217    /// be to rebuild the graph, the dominator tree and the forest and ask again. On a function with
218    /// sixteen hundred loops that is two thousand rebuilds, which was most of what an optimized
219    /// build of tamnd/rucc#1086's test spent its time on. What the rebuild protects is one loop's
220    /// shape, so this takes the loops whose shapes do not touch and copies all of their headers
221    /// from the one look. [`independent`] is the argument for why that is the same edit.
222    ///
223    /// `say` is false on every call after the first so that a loop this declines is declined once
224    /// rather than once per round.
225    fn plan(
226        &self,
227        func: &Func,
228        an: &mut Analyses,
229        done: &HashSet<Block>,
230        stats: &mut Stats,
231        say: bool,
232    ) -> Vec<Job> {
233        let (cfg, dom, loops) = (an.cfg(func), an.dominators(func), an.loops(func));
234        let mut wanted = Vec::new();
235        for id in loops.all() {
236            let header = loops.header(id);
237            if done.contains(&header) {
238                continue;
239            }
240            match self.consider(func, cfg, loops, id, header) {
241                Ok(candidate) => wanted.push(candidate),
242                Err(why) if say && why == ALREADY => stats.note(ALREADY),
243                Err(why) if say => stats.missed(why),
244                Err(_) => (),
245            }
246        }
247        let jobs = carried(func, dom, loops, wanted, stats, say);
248        independent(loops, jobs)
249    }
250
251    /// Whether this loop can have its header copied, and why not when it cannot.
252    ///
253    /// Everything here is answered out of the loop itself. What the header defines and who reads it
254    /// is the one question that is about the whole function, and [`carried`] asks it for all the
255    /// candidates at once.
256    fn consider(
257        &self,
258        func: &Func,
259        cfg: &Cfg,
260        loops: &Loops,
261        id: LoopId,
262        header: Block,
263    ) -> Result<Candidate, &'static str> {
264        let leaves = cfg.successors(header).iter().any(|&to| !loops.contains(id, to));
265        if !leaves {
266            // The exit test is somewhere below, which is the shape this pass is trying to reach.
267            // GCC asks the same question the other way round in `do_while_loop_p`.
268            return Err(ALREADY);
269        }
270        let entry = loops.preheader(cfg, id).ok_or(NO_PREHEADER)?;
271        let term = func.terminator(header).ok_or(SHAPE)?;
272        if func[term].opcode != Opcode::BrIf {
273            return Err(SHAPE);
274        }
275        let calls: Vec<BlockCall> = func.successors(term).collect();
276        let [then_call, else_call] = calls[..].try_into().map_err(|_| SHAPE)?;
277        let body = match (loops.contains(id, then_call.block), loops.contains(id, else_call.block))
278        {
279            (true, false) => then_call.block,
280            (false, true) => else_call.block,
281            _ => return Err(SHAPE),
282        };
283        if body == header {
284            return Err(SHAPE);
285        }
286        let insts: Vec<Inst> = func.insts(header).filter(|&inst| inst != term).collect();
287        if insts.len() > self.budget as usize {
288            return Err(TOO_BIG);
289        }
290        for &inst in &insts {
291            if !repeatable(func, inst) {
292                return Err(EFFECTS);
293            }
294        }
295        let mut defined: Vec<Value> = func[header].params.clone();
296        for &inst in &insts {
297            defined.extend(func[inst].results());
298        }
299        Ok(Candidate { id, header, entry, body, defined })
300    }
301}
302
303/// Whether an instruction may stand in a second copy of the block it is in.
304///
305/// Two questions rather than one. [`Opcode::has_effects`] is document 17.1's whitelist and is what
306/// section 26.8 names. The second is about this pass rather than about the program: an instruction
307/// carrying a side table entry is copied here by copying the index, which is right for an
308/// immediate, a symbol and a comparison because those tables are written once and read for ever,
309/// and is not something to assume about a table nobody has checked. So the copy is restricted to
310/// the payloads it has been thought about, and an instruction with any other is declined the same
311/// way one with an effect is.
312fn repeatable(func: &Func, inst: Inst) -> bool {
313    let data = func[inst];
314    if data.opcode.has_effects() || func.carries_mem(inst) {
315        return false;
316    }
317    matches!(
318        data.extra.kind(),
319        ExtraKind::None
320            | ExtraKind::Imm
321            | ExtraKind::Symbol
322            | ExtraKind::IntPred
323            | ExtraKind::FloatPred
324    )
325}
326
327/// Turns the candidates into jobs by working out, for each, which of the values its header defines
328/// the rest of its loop reads.
329///
330/// Those are what the copy owes a merge at the body. A value read outside the loop takes the
331/// candidate out rather than joining the list, because merging it would need a second parameter at
332/// the exit and after [`crate::canon`] there is no such value to merge: loop-closed form has already
333/// routed it.
334///
335/// One walk of the function for every candidate at once. tamnd/rucc#1015 made this one walk per
336/// candidate instead of one per value, and tamnd/rucc#1086 is the same move one level up: a header
337/// defines a handful of values, the function it is in can be very large, and a function with sixteen
338/// hundred loops in it was paying for sixteen hundred walks per round. A value belongs to exactly
339/// one candidate, because two candidates are two loops and two loops have two headers, so one map
340/// from value to candidate is enough to share the walk.
341fn carried(
342    func: &Func,
343    dom: &Dominators,
344    loops: &Loops,
345    wanted: Vec<Candidate>,
346    stats: &mut Stats,
347    say: bool,
348) -> Vec<Job> {
349    let mut watched: HashMap<Value, usize> = HashMap::new();
350    for (which, candidate) in wanted.iter().enumerate() {
351        for &value in &candidate.defined {
352            watched.insert(value, which);
353        }
354    }
355    let mut read: Vec<HashSet<Value>> = vec![HashSet::new(); wanted.len()];
356    let mut escapes = vec![false; wanted.len()];
357    let mut names: Vec<usize> = Vec::new();
358    for block in func.blocks() {
359        names.clear();
360        for inst in func.insts(block) {
361            reads(func, inst, block, &wanted, &watched, &mut read, &mut names);
362        }
363        for &which in &names {
364            let candidate = &wanted[which];
365            if !loops.contains(candidate.id, block) || !dom.dominates(candidate.body, block) {
366                escapes[which] = true;
367            }
368        }
369    }
370    let mut jobs = Vec::new();
371    for (which, candidate) in wanted.into_iter().enumerate() {
372        if escapes[which] {
373            if say {
374                stats.missed(ESCAPES);
375            }
376            continue;
377        }
378        let taken = &read[which];
379        let carried = candidate.defined.into_iter().filter(|value| taken.contains(value)).collect();
380        jobs.push(Job {
381            id: candidate.id,
382            header: candidate.header,
383            entry: candidate.entry,
384            body: candidate.body,
385            carried,
386        });
387    }
388    jobs
389}
390
391/// Records every watched value this instruction names, as an operand or on an edge out of it, and
392/// notes which candidates the block named something of.
393///
394/// Which candidates rather than which values, because a block that reads one of these from the wrong
395/// place is an error for that candidate whichever of its values it read. A candidate's own header is
396/// left out on both counts: the header is where these values are defined and reading one there is
397/// neither a carry nor an escape.
398fn reads(
399    func: &Func,
400    inst: Inst,
401    block: Block,
402    wanted: &[Candidate],
403    watched: &HashMap<Value, usize>,
404    read: &mut [HashSet<Value>],
405    names: &mut Vec<usize>,
406) {
407    let mut note = |value: Value| {
408        let Some(&which) = watched.get(&value) else { return };
409        if block == wanted[which].header {
410            return;
411        }
412        read[which].insert(value);
413        if !names.contains(&which) {
414            names.push(which);
415        }
416    };
417    for &value in &func[func[inst].args] {
418        note(value);
419    }
420    for call in func.successors(inst) {
421        for &value in &func[call.args] {
422            note(value);
423        }
424    }
425}
426
427/// The jobs out of a round that may all be applied before the function is looked at again.
428///
429/// Two jobs are safe together when the loops they are about share no block and neither loop holds
430/// the other's preheader. The argument is that a job writes only inside its own loop and to its own
431/// preheader. The copy is a new block put on the edge into the header, and the only block outside
432/// the loop it edits is the preheader, whose one successor is the header by the definition
433/// [`Loops::preheader`] uses. The merge gives the body a parameter and hands a value over on every
434/// edge into the body, and every one of those edges comes from inside the loop, because a natural
435/// loop is entered at its header alone and the body is not the header. The rewrite that follows
436/// reaches only the blocks that read the carried values, and [`carried`] has already taken the job
437/// out if any of those is outside the loop. So two jobs whose loops and preheaders do not meet edit
438/// two disjoint sets of blocks, and applying both from one look at the function is the same function
439/// as applying one, looking again, and applying the other.
440///
441/// Loops that share a block at all are nested, so the loops a taken one rules out are the ones it is
442/// nested in and the ones nested in it.
443fn independent(loops: &Loops, jobs: Vec<Job>) -> Vec<Job> {
444    let mut blocked = vec![false; loops.count()];
445    let mut taken = vec![false; loops.count()];
446    let mut kept: Vec<Job> = Vec::new();
447    for job in jobs {
448        if blocked[job.id.index()] || inside(loops, &taken, job.entry) {
449            continue;
450        }
451        let mut up = Some(job.id);
452        while let Some(id) = up {
453            blocked[id.index()] = true;
454            up = loops.parent(id);
455        }
456        let mut down = vec![job.id];
457        while let Some(id) = down.pop() {
458            blocked[id.index()] = true;
459            down.extend(loops.children(id));
460        }
461        // And no later job may be about a loop this one's preheader sits in.
462        let mut around = loops.innermost(job.entry);
463        while let Some(id) = around {
464            blocked[id.index()] = true;
465            around = loops.parent(id);
466        }
467        taken[job.id.index()] = true;
468        kept.push(job);
469    }
470    kept
471}
472
473/// Whether any loop holding this block has been taken already.
474fn inside(loops: &Loops, taken: &[bool], block: Block) -> bool {
475    let mut walk = loops.innermost(block);
476    while let Some(id) = walk {
477        if taken[id.index()] {
478            return true;
479        }
480        walk = loops.parent(id);
481    }
482    false
483}
484
485/// Makes the copy, puts it on the edge into the loop, and returns it.
486fn apply(func: &mut Func, job: &Job) -> Block {
487    let term = func.terminator(job.header).expect("the plan read this terminator");
488    let entry_term = func.terminator(job.entry).expect("a preheader ends in a jump");
489    // The header's parameters stand for whatever the one edge in hands them, so the copy is
490    // written in terms of those arguments and needs no parameters of its own.
491    let incoming = edge_args(func, entry_term, job.header);
492    let mut map: HashMap<Value, Value> = HashMap::new();
493    for (&param, &arg) in func[job.header].params.clone().iter().zip(&incoming) {
494        map.insert(param, arg);
495    }
496    let copy = func.create_block();
497    let insts: Vec<Inst> = func.insts(job.header).filter(|&inst| inst != term).collect();
498    for inst in insts {
499        clone_into(func, copy, inst, &mut map);
500    }
501    clone_branch(func, copy, term, &map);
502    for at in func.target_list(entry_term).iter() {
503        let call = func[at];
504        if call.block == job.header {
505            func.set_block_call(at, BlockCall { block: copy, args: ValueList::EMPTY, ..call });
506        }
507    }
508    for &value in &job.carried {
509        let arrived = map.get(&value).copied().unwrap_or(value);
510        merge(func, job, copy, value, arrived);
511    }
512    copy
513}
514
515/// The arguments a terminator hands one of its targets.
516fn edge_args(func: &Func, term: Inst, to: Block) -> Vec<Value> {
517    for call in func.successors(term) {
518        if call.block == to {
519            return func[call.args].to_vec();
520        }
521    }
522    Vec::new()
523}
524
525/// Copies one instruction to the end of a block, under the substitution, and records its results.
526fn clone_into(func: &mut Func, into: Block, inst: Inst, map: &mut HashMap<Value, Value>) {
527    let data = func[inst];
528    let args: Vec<Value> =
529        func[data.args].iter().map(|value| map.get(value).copied().unwrap_or(*value)).collect();
530    let types: Vec<Type> = data.results().map(|result| func[result].ty).collect();
531    let span = func.span(inst);
532    let args = func.push_values(&args);
533    let fresh = func.create_inst(InstData { args, ..data }, &types, span);
534    func.append_inst(into, fresh);
535    for (old, new) in data.results().zip(func[fresh].results()) {
536        map.insert(old, new);
537    }
538}
539
540/// Copies the header's two way branch to the end of the copy, under the substitution.
541///
542/// The targets are the header's own. What that means for the graph is that the copy decides,
543/// before the loop, which of the two places the header would have gone control goes to, and the
544/// header is left deciding it for every iteration after the first.
545fn clone_branch(func: &mut Func, into: Block, term: Inst, map: &HashMap<Value, Value>) {
546    let at = |value: &Value| map.get(value).copied().unwrap_or(*value);
547    let cond = at(&func[func[term].args][0]);
548    let calls: Vec<BlockCall> = func.successors(term).collect();
549    let args: Vec<Vec<Value>> =
550        calls.iter().map(|call| func[call.args].iter().map(at).collect()).collect();
551    Builder::new(func, into).br_if(cond, calls[0].block, &args[0], calls[1].block, &args[1]);
552}
553
554/// Gives the body a parameter for a value the header defines, and points the loop at it.
555///
556/// Three kinds of edge arrive at the body once the copy is in place. The header's carries what the
557/// header worked out, which is the value on every iteration after the first. The copy's carries
558/// what the copy worked out, which is the value on the first. Anything else is a block inside the
559/// loop, and what is current there is the parameter itself, which is available because the body
560/// dominates the whole loop the moment the copy is the only way in.
561fn merge(func: &mut Func, job: &Job, copy: Block, value: Value, arrived: Value) {
562    let param = func.append_param(job.body, func[value].ty);
563    for block in func.blocks().collect::<Vec<_>>() {
564        let Some(term) = func.terminator(block) else { continue };
565        let carry = if block == job.header {
566            value
567        } else if block == copy {
568            arrived
569        } else {
570            param
571        };
572        for at in func.target_list(term).iter() {
573            let call = func[at];
574            if call.block != job.body {
575                continue;
576            }
577            let args = func.append_arg(call.args, carry);
578            func.set_block_call(at, BlockCall { args, ..call });
579        }
580    }
581    // Everything the header used to reach reads the parameter now. The header itself does not:
582    // what it hands the body is still its own definition, and that is the edge the parameter was
583    // put there to distinguish.
584    for block in func.blocks().collect::<Vec<_>>() {
585        if block == job.header || block == copy {
586            continue;
587        }
588        for inst in func.insts(block).collect::<Vec<_>>() {
589            let swap = |had: Value| if had == value { param } else { had };
590            func.rewrite(func[inst].args, swap);
591            for at in func.target_list(inst).iter() {
592                func.rewrite(func[at].args, swap);
593            }
594        }
595    }
596}
597
598/// Asks the ranges whether the copied tests are settled, and takes out the ones that are.
599///
600/// This is section 26.6's point about the entry condition. A test that always holds leaves a loop
601/// known to run at least once, which is what document 07.5's trip count wanted. One that never
602/// holds leaves the loop unreachable, and taking it out is [`crate::simplify_cfg::sweep`]'s job
603/// rather than this one's.
604///
605/// Every copy the round made is asked from the one set of ranges, and the answers are acted on
606/// afterwards. That is sound because every question is about a different block's terminator and
607/// every answer is a fact about the values arriving there, which taking a branch out somewhere else
608/// cannot make untrue. Asking one at a time would mean a graph and a dominator tree per copy, which
609/// is the cost tamnd/rucc#1086 is about.
610///
611/// Answers whether it moved an edge, which the caller needs because the analyses this built are the
612/// ones the next round wants and they are only stale if a branch came out. The ranges settle the
613/// test on a minority of the loops here and the graph is the size of the function, so the rounds
614/// where nothing happens used to pay for a rebuild that changed nothing. tamnd/rucc#1045.
615fn settle(func: &mut Func, an: &mut Analyses, copies: &[Block], stats: &mut Stats) -> bool {
616    let mut out: Vec<(Inst, BlockCall, bool)> = Vec::new();
617    {
618        let cfg = an.cfg(func);
619        let dom = an.dominators(func);
620        let mut ranges = Ranges::new(func, cfg, dom);
621        for &copy in copies {
622            let Some(term) = func.terminator(copy) else { continue };
623            let cond = func[func[term].args][0];
624            let Some(taken) = prune::settled(func, &mut ranges, copy, cond) else {
625                stats.missed(UNDECIDED);
626                continue;
627            };
628            let calls: Vec<BlockCall> = func.successors(term).collect();
629            out.push((term, if taken { calls[0] } else { calls[1] }, taken));
630        }
631    }
632    if out.is_empty() {
633        return false;
634    }
635    for (term, call, taken) in out {
636        simplify_cfg::jump_to(func, term, call);
637        stats.optimized(if taken { ENTERED } else { SKIPPED });
638    }
639    true
640}
641
642#[cfg(test)]
643mod tests {
644    use rucc_base::Interner;
645    use rucc_ir::{
646        Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
647        Signature, Type, verify_func,
648    };
649    use rucc_target::{TargetInfo, Triple};
650
651    use super::{HeaderCopy, SIZE, SPEED};
652    use crate::canon::Canon;
653    use crate::cfg::Cfg;
654    use crate::dom::Dominators;
655    use crate::loops::Loops;
656    use crate::stats::Kind;
657    use crate::{Fuel, Pass, Stats};
658
659    /// Canonicalizes and then copies, with as much fuel as both want.
660    ///
661    /// Both, because the pass is written against the shape [`Canon`] leaves and running it over
662    /// anything else is a test of a situation the pipeline does not produce. Section 26.7 puts the
663    /// two next to each other in that order and so does this.
664    fn copied(func: &mut Func, pass: &HeaderCopy) -> Stats {
665        let mut an = crate::machine::fixtures::analyses();
666        Canon.run(func, &mut an, &mut Fuel::unlimited());
667        pass.run(func, &mut an, &mut Fuel::unlimited())
668    }
669
670    /// The forest of the function as it is now.
671    fn forest(func: &Func) -> (Cfg, Dominators, Loops) {
672        let cfg = Cfg::new(func);
673        let dom = Dominators::new(&cfg);
674        let loops = Loops::new(&cfg, &dom);
675        (cfg, dom, loops)
676    }
677
678    /// Insists the function is one the rest of the compiler may believe.
679    ///
680    /// This is where most of the strength of these tests is. The copy gives the loop a second way
681    /// in, which is exactly the edit that breaks a definition's dominance over its uses, and the
682    /// verifier is what says whether the merge the pass wrote is the merge the graph needed.
683    fn sound(func: &Func, names: &mut Interner) {
684        let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
685        let module = Module::new(names.intern("t.c"), &target);
686        if let Err(errors) = verify_func(&module, func, names) {
687            panic!("{errors:#?}");
688        }
689    }
690
691    /// A counted loop that tests at the top, which is what `while (i < n)` lowers to.
692    ///
693    /// ```text
694    /// entry: i0 = 0; jump head(i0)
695    /// head(i): t = i < n; br t -> body, done
696    /// body: next = i + 1; jump head(next)
697    /// done: ret i
698    /// ```
699    ///
700    /// `bound` is the limit as a constant, or nothing for a limit the function was handed and
701    /// which the ranges therefore cannot settle.
702    fn counted(bound: Option<i128>) -> (Func, Interner, Vec<Block>) {
703        let mut names = Interner::new();
704        let params: &[Type] = if bound.is_some() { &[] } else { &[Type::int(32)] };
705        let signature = Signature::new().with_params(params).with_returns(&[Type::int(32)]);
706        let mut func = Func::new(names.intern("f"), signature);
707        let entry = func.create_block();
708        let head = func.create_block();
709        let body = func.create_block();
710        let done = func.create_block();
711        let limit = match bound {
712            Some(value) => Builder::new(&mut func, entry).iconst(Type::int(32), value),
713            None => func.append_param(entry, Type::int(32)),
714        };
715        let i = func.append_param(head, Type::int(32));
716        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
717        Builder::new(&mut func, entry).jump(head, &[zero]);
718        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
719        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
720        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
721        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
722        Builder::new(&mut func, body).jump(head, &[next]);
723        Builder::new(&mut func, done).ret(&[i]);
724        (func, names, vec![entry, head, body, done])
725    }
726
727    /// Whether the header of the only loop leaves it, which is the question section 26.6 is about.
728    fn tests_at_the_top(func: &Func) -> bool {
729        let (cfg, dom, loops) = forest(func);
730        let _ = dom;
731        let id = loops.all().next().expect("there is a loop");
732        let header = loops.header(id);
733        cfg.successors(header).iter().any(|&to| !loops.contains(id, to))
734    }
735
736    #[test]
737    fn a_loop_that_tests_at_the_top_ends_up_testing_at_the_bottom() {
738        let (mut func, mut names, _) = counted(None);
739        assert!(tests_at_the_top(&func), "the shape this pass is for");
740
741        let stats = copied(&mut func, &SPEED);
742        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
743        assert!(!tests_at_the_top(&func), "the header no longer leaves the loop");
744        sound(&func, &mut names);
745    }
746
747    #[test]
748    fn the_value_the_header_defined_is_merged_where_the_two_ways_in_meet() {
749        let (mut func, mut names, blocks) = counted(None);
750        let body = blocks[2];
751        assert!(func[body].params.is_empty(), "the body carries nothing to start with");
752
753        copied(&mut func, &SPEED);
754        assert_eq!(func[body].params.len(), 1, "the counter arrives as a parameter now");
755        assert_eq!(
756            Cfg::new(&func).predecessors(body).len(),
757            2,
758            "one edge from the header and one from the copy"
759        );
760        sound(&func, &mut names);
761    }
762
763    #[test]
764    fn an_entry_test_the_ranges_settle_is_taken_out() {
765        let (mut func, mut names, _) = counted(Some(10));
766
767        let stats = copied(&mut func, &SPEED);
768        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
769        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 1);
770        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 0);
771        sound(&func, &mut names);
772
773        let (cfg, _dom, loops) = forest(&func);
774        let id = loops.all().next().expect("the loop is still there");
775        let entry = func.entry().expect("there is an entry");
776        assert!(cfg.reaches(loops.header(id)), "and it is still reached");
777        assert_eq!(cfg.successors(entry).len(), 1, "the guard in front of it has gone");
778    }
779
780    #[test]
781    fn a_loop_the_ranges_say_never_runs_is_removed() {
782        let (mut func, mut names, blocks) = counted(Some(0));
783
784        let stats = copied(&mut func, &SPEED);
785        assert_eq!(stats.count(Kind::Optimized, super::SKIPPED), 1);
786        sound(&func, &mut names);
787
788        let (_cfg, _dom, loops) = forest(&func);
789        assert_eq!(loops.count(), 0, "there is no loop left");
790        assert!(!func.blocks().any(|block| block == blocks[2]), "and the body has gone with it");
791    }
792
793    #[test]
794    fn a_test_the_ranges_cannot_settle_leaves_the_guard_where_it_is() {
795        let (mut func, _names, _) = counted(None);
796
797        let stats = copied(&mut func, &SPEED);
798        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
799        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 1);
800        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 0);
801    }
802
803    #[test]
804    fn a_second_run_changes_nothing() {
805        let (mut func, mut names, _) = counted(None);
806        copied(&mut func, &SPEED);
807        let again =
808            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
809        assert_eq!(again.count(Kind::Optimized, super::COPIED), 0, "there is nothing left to do");
810        assert_eq!(again.count(Kind::Note, super::ALREADY), 1, "and it says why");
811        sound(&func, &mut names);
812    }
813
814    #[test]
815    fn a_header_that_writes_to_memory_is_left_alone() {
816        let mut names = Interner::new();
817        let signature = Signature::new().with_params(&[Type::int(32), Type::PTR]).with_returns(&[]);
818        let mut func = Func::new(names.intern("f"), signature);
819        let entry = func.create_block();
820        let head = func.create_block();
821        let body = func.create_block();
822        let done = func.create_block();
823        let limit = func.append_param(entry, Type::int(32));
824        let addr = func.append_param(entry, Type::PTR);
825        let i = func.append_param(head, Type::int(32));
826        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
827        Builder::new(&mut func, entry).jump(head, &[zero]);
828        let access = MemInfo {
829            size: 4,
830            align: 4,
831            order: MemOrder::NotAtomic,
832            tbaa: None,
833            owns: 0,
834            restrict: Restrict::NONE,
835        };
836        Builder::new(&mut func, head).store(i, addr, access, Flags::NONE);
837        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
838        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
839        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
840        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
841        Builder::new(&mut func, body).jump(head, &[next]);
842        Builder::new(&mut func, done).ret(&[]);
843
844        let stats = copied(&mut func, &SPEED);
845        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
846        assert_eq!(stats.count(Kind::Missed, super::EFFECTS), 1);
847        assert!(tests_at_the_top(&func), "the loop is exactly as it was");
848    }
849
850    #[test]
851    fn a_header_larger_than_the_level_allows_is_left_alone() {
852        // Seven instructions in the header, which is over the size budget and well under the
853        // speed one, so the two instances of the pass disagree about the same function.
854        let stats = copied(&mut padded(6), &SIZE);
855        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
856        assert_eq!(stats.count(Kind::Missed, super::TOO_BIG), 1);
857
858        let stats = copied(&mut padded(6), &SPEED);
859        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1, "the speed budget is wider");
860    }
861
862    /// The counted loop with that many more instructions in its header, which do nothing.
863    fn padded(extra: usize) -> Func {
864        let (mut func, _names, blocks) = counted(None);
865        let head = blocks[1];
866        let term = func.terminator(head).expect("the header branches");
867        for _ in 0..extra {
868            let filler = Builder::new(&mut func, head).iconst(Type::int(32), 7);
869            let Def::Result { inst, .. } = func[filler].def else { unreachable!("an iconst") };
870            func.remove_inst(inst);
871            func.insert_before(inst, term);
872        }
873        func
874    }
875
876    #[test]
877    fn fuel_stops_the_copy_where_it_stands() {
878        let (mut func, _names, _) = counted(None);
879        let mut an = crate::machine::fixtures::analyses();
880        Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
881
882        let stats = SPEED.run(&mut func, &mut an, &mut Fuel::of(0));
883        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
884        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
885        assert!(tests_at_the_top(&func), "and the loop is as it was");
886    }
887
888    #[test]
889    fn a_value_the_header_defines_and_the_code_after_the_loop_reads_is_declined() {
890        // Straight to the copy, so loop-closed form has not been established and the counter the
891        // return names is still the header's own definition. That is the one value this pass will
892        // not merge, and section 26.7's answer is that [`Canon`] has already routed it by the time
893        // the pipeline gets here.
894        let (mut func, _names, _) = counted(None);
895
896        let stats =
897            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
898        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
899        assert_eq!(stats.count(Kind::Missed, super::ESCAPES), 1);
900        assert!(tests_at_the_top(&func), "and the loop is as it was");
901    }
902
903    #[test]
904    fn a_loop_with_no_preheader_is_declined() {
905        let mut names = Interner::new();
906        let signature = Signature::new().with_params(&[Type::int(1), Type::int(32)]);
907        let mut func = Func::new(names.intern("f"), signature);
908        let entry = func.create_block();
909        let one = func.create_block();
910        let two = func.create_block();
911        let head = func.create_block();
912        let body = func.create_block();
913        let done = func.create_block();
914        let c = func.append_param(entry, Type::int(1));
915        let limit = func.append_param(entry, Type::int(32));
916        let i = func.append_param(head, Type::int(32));
917        Builder::new(&mut func, entry).br_if(c, one, &[], two, &[]);
918        let zero = Builder::new(&mut func, one).iconst(Type::int(32), 0);
919        Builder::new(&mut func, one).jump(head, &[zero]);
920        let start = Builder::new(&mut func, two).iconst(Type::int(32), 1);
921        Builder::new(&mut func, two).jump(head, &[start]);
922        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
923        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
924        Builder::new(&mut func, body).jump(head, &[i]);
925        Builder::new(&mut func, done).ret(&[]);
926
927        let stats =
928            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
929        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
930        assert_eq!(stats.count(Kind::Missed, super::NO_PREHEADER), 1);
931
932        // And with one, which is what the pipeline hands it, the same loop is copied.
933        let stats = copied(&mut func, &SPEED);
934        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
935        sound(&func, &mut names);
936    }
937
938    /// Two counted loops one after the other, which share no block and no preheader.
939    ///
940    /// ```text
941    /// entry(n): jump one(0)
942    /// one(i):  t = i < n; br t -> up, mid
943    /// up:      i2 = i + 1; jump one(i2)
944    /// mid:     jump two(0)
945    /// two(j):  u = j < n; br u -> down, done
946    /// down:    j2 = j + 1; jump two(j2)
947    /// done:    ret
948    /// ```
949    fn side_by_side() -> (Func, Interner, Vec<Block>) {
950        let mut names = Interner::new();
951        let signature = Signature::new().with_params(&[Type::int(32)]);
952        let mut func = Func::new(names.intern("f"), signature);
953        let entry = func.create_block();
954        let one = func.create_block();
955        let up = func.create_block();
956        let mid = func.create_block();
957        let two = func.create_block();
958        let down = func.create_block();
959        let done = func.create_block();
960        let n = func.append_param(entry, Type::int(32));
961        let i = func.append_param(one, Type::int(32));
962        let j = func.append_param(two, Type::int(32));
963        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
964        Builder::new(&mut func, entry).jump(one, &[zero]);
965        let t = Builder::new(&mut func, one).icmp(IntPred::Slt, i, n);
966        Builder::new(&mut func, one).br_if(t, up, &[], mid, &[]);
967        let step = Builder::new(&mut func, up).iconst(Type::int(32), 1);
968        let next = Builder::new(&mut func, up).binary(Opcode::Add, i, step, Flags::NONE);
969        Builder::new(&mut func, up).jump(one, &[next]);
970        let start = Builder::new(&mut func, mid).iconst(Type::int(32), 0);
971        Builder::new(&mut func, mid).jump(two, &[start]);
972        let u = Builder::new(&mut func, two).icmp(IntPred::Slt, j, n);
973        Builder::new(&mut func, two).br_if(u, down, &[], done, &[]);
974        let stride = Builder::new(&mut func, down).iconst(Type::int(32), 1);
975        let after = Builder::new(&mut func, down).binary(Opcode::Add, j, stride, Flags::NONE);
976        Builder::new(&mut func, down).jump(two, &[after]);
977        Builder::new(&mut func, done).ret(&[]);
978        (func, names, vec![up, down])
979    }
980
981    #[test]
982    fn two_loops_that_do_not_meet_are_both_copied() {
983        let (mut func, mut names, bodies) = side_by_side();
984
985        let stats = copied(&mut func, &SPEED);
986        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 2);
987        sound(&func, &mut names);
988
989        let (_cfg, _dom, loops) = forest(&func);
990        assert_eq!(loops.count(), 2, "both loops are still loops");
991        for id in loops.all() {
992            let header = loops.header(id);
993            assert!(
994                !Cfg::new(&func).successors(header).iter().any(|&to| !loops.contains(id, to)),
995                "and neither of them tests at the top any more"
996            );
997        }
998        for body in bodies {
999            assert_eq!(func[body].params.len(), 1, "each body carries its own counter");
1000        }
1001    }
1002
1003    /// A counted loop with a counted loop inside it, where the inner preheader is an outer block.
1004    ///
1005    /// ```text
1006    /// entry(n): jump outer(0)
1007    /// outer(i): t = i < n; br t -> ahead, done
1008    /// ahead:    jump inner(0)
1009    /// inner(j): u = j < n; br u -> under, latch
1010    /// under:    j2 = j + 1; jump inner(j2)
1011    /// latch:    i2 = i + 1; jump outer(i2)
1012    /// done:     ret
1013    /// ```
1014    fn nested() -> (Func, Interner) {
1015        let mut names = Interner::new();
1016        let signature = Signature::new().with_params(&[Type::int(32)]);
1017        let mut func = Func::new(names.intern("f"), signature);
1018        let entry = func.create_block();
1019        let outer = func.create_block();
1020        let ahead = func.create_block();
1021        let inner = func.create_block();
1022        let under = func.create_block();
1023        let latch = func.create_block();
1024        let done = func.create_block();
1025        let n = func.append_param(entry, Type::int(32));
1026        let i = func.append_param(outer, Type::int(32));
1027        let j = func.append_param(inner, Type::int(32));
1028        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
1029        Builder::new(&mut func, entry).jump(outer, &[zero]);
1030        let t = Builder::new(&mut func, outer).icmp(IntPred::Slt, i, n);
1031        Builder::new(&mut func, outer).br_if(t, ahead, &[], done, &[]);
1032        let start = Builder::new(&mut func, ahead).iconst(Type::int(32), 0);
1033        Builder::new(&mut func, ahead).jump(inner, &[start]);
1034        let u = Builder::new(&mut func, inner).icmp(IntPred::Slt, j, n);
1035        Builder::new(&mut func, inner).br_if(u, under, &[], latch, &[]);
1036        let stride = Builder::new(&mut func, under).iconst(Type::int(32), 1);
1037        let after = Builder::new(&mut func, under).binary(Opcode::Add, j, stride, Flags::NONE);
1038        Builder::new(&mut func, under).jump(inner, &[after]);
1039        let step = Builder::new(&mut func, latch).iconst(Type::int(32), 1);
1040        let next = Builder::new(&mut func, latch).binary(Opcode::Add, i, step, Flags::NONE);
1041        Builder::new(&mut func, latch).jump(outer, &[next]);
1042        Builder::new(&mut func, done).ret(&[]);
1043        (func, names)
1044    }
1045
1046    #[test]
1047    fn a_loop_and_the_loop_inside_it_are_copied_one_round_apart() {
1048        // The two share every block the inner one has, and the inner one's preheader is a block of
1049        // the outer one, so a round may hold at most one of them. Both are still copied, and the
1050        // point of the test is that the second is planned against a function the first has already
1051        // changed rather than against the plan the first was made from.
1052        let (mut func, mut names) = nested();
1053
1054        let stats = copied(&mut func, &SPEED);
1055        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 2);
1056        sound(&func, &mut names);
1057
1058        let (cfg, _dom, loops) = forest(&func);
1059        assert_eq!(loops.count(), 2, "both loops survived the copy");
1060        for id in loops.all() {
1061            let header = loops.header(id);
1062            assert!(
1063                !cfg.successors(header).iter().any(|&to| !loops.contains(id, to)),
1064                "and both test at the bottom now"
1065            );
1066        }
1067    }
1068
1069    #[test]
1070    fn a_round_stops_where_the_fuel_does() {
1071        // Two loops a round may hold together, and one unit of fuel. The first is copied and the
1072        // second is left for a run with more, which is what taking fuel per job rather than per
1073        // round means.
1074        let (mut func, mut names) = {
1075            let (mut func, names, _) = side_by_side();
1076            let mut an = crate::machine::fixtures::analyses();
1077            Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
1078            (func, names)
1079        };
1080
1081        let stats =
1082            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(1));
1083        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
1084        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
1085        sound(&func, &mut names);
1086    }
1087}