Skip to main content

rucc_codegen/
schedule.rs

1//! Putting the instructions of a block in the order that finishes soonest.
2//!
3//! Design: `spec/optimizer/38-scheduling-and-layout.md` sections 38.1, 38.6 and 38.7.
4//!
5//! Every instruction in a block is going to run, in some order, and the orders that compute the
6//! same thing are the ones that keep each instruction behind the ones it reads from. Among those
7//! orders, one finishes before the others, because the machine does not answer every instruction in
8//! one cycle: a multiply takes three, a load takes five, and an instruction that reads what one of
9//! them wrote cannot start until it is done. Putting independent work in those cycles rather than
10//! waiting is the whole of this pass.
11//!
12//! # The algorithm, and where it comes from
13//!
14//! A list scheduler, which is `gcc/haifa-sched.cc`'s. Build a graph of what depends on what, take
15//! the instructions whose dependences are all satisfied, choose one, repeat. Everything a scheduler
16//! is is in how it chooses, and `gcc/haifa-sched.cc:55` writes that out as a list of eight
17//! tiebreaks. Section 38.1 goes through them and says which are rucc's: one, two, six, seven and
18//! eight. Three, four and five are about moving instructions between blocks and about moving them
19//! where they might not have run, and this pass does neither.
20//!
21//! So what this pass chooses by is five numbers in that order:
22//!
23//! 1. The longest path from here to the end of the run, in cycles. This is the criterion, and the
24//!    other four are for when it ties. An instruction on the critical path delays everything behind
25//!    it by exactly as much as it is delayed, and one that is not on it is free until it is.
26//! 2. How many more registers are live after it than before. Section 38.1 quotes
27//!    `gcc/haifa-sched.cc:87` on what this is for: "if an operation requires that constants be
28//!    loaded into registers, it is certainly desirable to load those constants as early as
29//!    necessary, but no earlier". An instruction that writes a register and reads nothing that dies
30//!    is one whose value now has to be kept somewhere, and hoisting it to the top of a block
31//!    because it depends on nothing is the classic way a scheduler makes a function worse.
32//! 3. Whether it reads what the instruction just scheduled wrote. It does not have to, since the
33//!    graph would have stopped it if it were not allowed, but one that does will wait and one that
34//!    does not will not.
35//! 4. How many instructions depend on it. Scheduling one of these makes more work available to
36//!    choose from later, which is what keeps the ready list from running dry.
37//! 5. Where it was to start with. This is not a heuristic. It is what makes the output a function
38//!    of the input, and it has to be a position rather than anything that comes out of a hash map,
39//!    for the reason `spec/10-backend.md` gives about a compiler whose output moves between runs.
40//!
41//! # Why it runs after the registers are handed out
42//!
43//! Section 38.6 decides it: "One scheduler, after allocation, before the layout freeze." The
44//! argument section 38.7 makes for that placement is the one that matters here. The dominant way a
45//! scheduler makes a program worse is by holding more values live at once than there are registers,
46//! so the allocator spills, and the spill costs more than the latency the schedule hid. After
47//! allocation that cannot happen: every value is already in a register, no reordering this pass can
48//! make changes which register anything is in, and nothing is left that could decide to spill.
49//!
50//! What it costs is that the registers are the constraint instead. Before allocation a value is
51//! written once, so the only dependence between two instructions is that one reads what the other
52//! wrote. Afterwards the same register holds a dozen different values over a block, so an
53//! instruction that writes one has to stay behind everything that reads what was in it, and those
54//! orderings are real even though no value passes between the two instructions. That is most of
55//! what the graph below is made of, and it is why this pass finds less to do than one before
56//! allocation would.
57//!
58//! How much less has now been measured, and the honest answer is almost all of it. Five programs
59//! built with this on and with it off, best of five runs each, on a six core Xeon with gcc 16 as
60//! the reference:
61//!
62//! ```text
63//! program    off    on   accurate   gcc-16 -O2
64//! ilp         75    75     78          60
65//! serial     213   212    215          58
66//! mem         47    49     49          40
67//! fp         271   267    266         108
68//! branchy    119   122    121          81
69//! ```
70//!
71//! Milliseconds, and the run to run spread on this machine is a few of them, so every column here
72//! is the same column. That is the measurement section 38.8 asked for and it says this pass is
73//! currently worth nothing on these five programs. Two reasons, and the first is the one above: by
74//! the time this runs the registers have been handed out, so the same register holds a dozen values
75//! over a block and the anti and output edges that creates pin most of the order in place. The
76//! second is that the gap to gcc is not a scheduling gap. A factor of three and a half on `serial`
77//! and two and a half on `fp` is work gcc did before it got anywhere near an instruction order, and
78//! no permutation of the instructions rucc emits closes it.
79//!
80//! The pass stays, at `-O2` and above, for what it costs rather than for what it currently returns:
81//! it is sound, it is cheap, and it is the thing that has to exist before the latencies in
82//! [`rucc_target::TimingInsts`] mean anything at all. The column worth watching is `accurate`, which
83//! is the same model told to believe its own unit counts, and which is slightly worse on the one
84//! program with real instruction level parallelism in it. That is the model being wrong about units
85//! in exactly the way [`rucc_target::TimingInsts::accurate`] says it is, and it is why x86-64
86//! answers `false`.
87//!
88//! # What the graph is made of
89//!
90//! Four kinds of edge, and the first three are `gcc/sched-deps.cc`'s `REG_DEP_TRUE`,
91//! `REG_DEP_OUTPUT` and `REG_DEP_ANTI` over registers:
92//!
93//! - One instruction reads a register another wrote, so it waits for the value.
94//! - Two instructions write the same register, so they stay in order or the register ends up
95//!   holding the wrong one of them.
96//! - One instruction writes a register another read, so the read stays in front of the write.
97//!
98//! The fourth is the condition state, which on this kind of machine is a register nobody named. It
99//! is not in an operand vector, so the three kinds above do not see it, and the target says which
100//! instructions write it and which read it. Getting this wrong is a miscompile and the failure
101//! looks like a target description that forgot a clobber, which section 38.7 says is the same root
102//! cause as every other missing-clobber bug.
103//!
104//! # Memory, and why it is one chain
105//!
106//! Every instruction that touches memory or computes an address stays in the order it was in,
107//! relative to every other one. That is stronger than it has to be. `gcc/haifa-sched.cc:71` is
108//! candid about the trade: "only if we can be certain that memory references are not part of the
109//! data dependency graph... can we move operations past memory references. To first approximation,
110//! reads can be done independently, while writes introduce dependencies."
111//!
112//! rucc cannot take the first approximation here. Machine IR does not carry `volatile`, which
113//! [`crate::copies`] says at length: a read the program insisted on and an ordinary one are the
114//! same instruction with the same operands by the time this runs. So two reads are not
115//! interchangeable either, and the only safe answer at this level is to leave the accesses in the
116//! order they arrived in. That is also the answer [`crate::combine`] gives, for the same reason and
117//! through the same question to the target.
118//!
119//! Address computation is in the chain as well, and not because an address is a memory access. It
120//! is because the stack pointer moves without saying so. A push and a pop change it and name it in
121//! no operand, so an address counted from it means different things on either side of one, and
122//! anything that carries an addressing mode is something that could be counted from it. Putting
123//! them all in one chain costs a little freedom around `lea` and needs no new question of the
124//! target.
125//!
126//! # What nothing moves across
127//!
128//! A call, because what a call does to memory and to the registers a convention does not preserve
129//! is not in its operands. An instruction the target does not describe, on the same reasoning
130//! backwards. An instruction the target describes as doing something the timing model does not
131//! cover, which is [`Unit::Fixed`]: a fence, a trap, a landing pad, the padding a patcher was
132//! promised. And an instruction that carries a frame rule, because those rules say what the
133//! unwinder should believe at each address in the prologue and the epilogue, and an instruction
134//! that moves takes its rule with it to an address where it is not true.
135//!
136//! The last instruction of a block, as well, along with whatever the caller has pinned. What a
137//! block leaves on is the last thing in it by the time [`crate::layout`] runs, and the layout is
138//! what turns the arms of a block into jumps, so a block whose condition is not at the end of it is
139//! a block the layout cannot write. What the caller pins is the comparison the layout is going to
140//! fuse with that condition, since the two have to stay next to each other for the fusion to
141//! happen and nothing here would otherwise keep them there.
142//!
143//! Each of those splits the block into runs, and a run is scheduled on its own with everything
144//! before and after it left where it was. A block with no barrier in it is one run.
145//!
146//! # The bound
147//!
148//! [`READY`] instructions are considered at each step and no more, which is
149//! `gcc/params.opt:761`'s `max-sched-ready-insns`, `Init(100)`, and section 38.8 asks for the same
150//! bound for the same reason: choosing is linear in the ready list and the ready list can be as
151//! long as the block. [`LONGEST`] is the second half of it, a run this pass will not build a graph
152//! for at all, because building one is quadratic in the worst case and a block of several thousand
153//! machine instructions is a generated table rather than something anybody is waiting on.
154//!
155//! # What makes it correct
156//!
157//! The order this writes is a topological order of the graph, and nothing else about the pass is
158//! load bearing. The timing model chooses among the orders the graph allows and cannot choose one
159//! it does not allow, so a model that is wrong about every number produces a slower program and not
160//! a different one, which is what spec 10.5 says the right failure mode is. What has to be right is
161//! the graph, and what makes the graph right is that every edge the machine needs is in it.
162
163use std::collections::{BTreeSet, HashMap, HashSet};
164
165use rucc_base::Interner;
166use rucc_mir::{Block, Func, Inst, Reg, Role};
167use rucc_target::{FlagInsts, MachineInsts, RegClass, Timing, TimingInsts, Unit};
168
169/// A register as the graph keys on it: the number and the file it is in.
170///
171/// The number on its own is not enough. A [`Reg`] that has been through the allocator is a place on
172/// the machine, and a machine numbers the places in each of its files from zero, so the first
173/// integer register and the first vector register are the same number and not the same place. A
174/// graph keyed on the number alone would chain a block's floating point work to the integer work
175/// beside it for no reason, which costs a schedule and is not wrong. A virtual register has one
176/// class for its whole life, so for anything that has not been through the allocator the pair says
177/// exactly what the number alone would.
178type Place = (Reg, RegClass);
179
180/// How many instructions are looked at when choosing the next one.
181///
182/// `gcc/params.opt:761`'s `max-sched-ready-insns`, `Init(100)`, and the same number for the same
183/// reason. The ones looked at are the ones that were earliest in the input, so the bound is a
184/// function of the input like everything else here.
185pub const READY: usize = 100;
186
187/// The longest run of instructions this will schedule.
188///
189/// Building the graph is quadratic in the worst case, since an instruction that writes a register
190/// has to be put behind every instruction that read it. A run longer than this is left exactly as
191/// it arrived.
192pub const LONGEST: usize = 2000;
193
194/// What one function came to.
195#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
196pub struct Scheduled {
197    /// Runs of instructions a schedule was chosen for.
198    pub runs: usize,
199    /// Instructions that came out somewhere other than where they went in.
200    pub moved: usize,
201}
202
203/// Puts each block's instructions in the order the machine finishes soonest.
204///
205/// `accurate` is whether the unit counts in the model are worth holding an instruction back over,
206/// which is `cycle-accurate-model` of section 38.1. A model that is not cycle accurate is one whose
207/// latencies came out of a table and whose picture of the machine's units is a summary, so the
208/// latencies are used to order and the units are not used to stall. See [`TimingInsts::accurate`].
209///
210/// `pinned` is the instructions the caller needs left where they are. The block's own last
211/// instruction is always one, and the caller adds the comparisons [`crate::layout`] is going to
212/// fuse with a branch, which have to stay next to the branch for the fusion to happen.
213pub fn insts(
214    func: &mut Func,
215    timing: &TimingInsts,
216    machine: &MachineInsts,
217    flags: &FlagInsts,
218    names: &Interner,
219    accurate: bool,
220    pinned: &HashSet<Inst>,
221) -> Scheduled {
222    let blocks: Vec<Block> = func.blocks().collect();
223    let mut done = Scheduled::default();
224    for block in blocks {
225        let was: Vec<Inst> = func.insts(block).collect();
226        if was.len() < 3 {
227            continue;
228        }
229        let mut now: Vec<Inst> = Vec::with_capacity(was.len());
230        let mut run: Vec<Inst> = Vec::new();
231        let last = was.last().copied();
232        for &inst in &was {
233            if Some(inst) == last
234                || pinned.contains(&inst)
235                || barrier(func, inst, timing, machine, names)
236            {
237                done.runs += usize::from(order(
238                    func, &run, timing, machine, flags, names, accurate, &mut now,
239                ));
240                run.clear();
241                now.push(inst);
242            } else {
243                run.push(inst);
244            }
245        }
246        done.runs +=
247            usize::from(order(func, &run, timing, machine, flags, names, accurate, &mut now));
248        let moved = was.iter().zip(&now).filter(|(before, after)| before != after).count();
249        if moved == 0 {
250            continue;
251        }
252        done.moved += moved;
253        for &inst in &was {
254            func.remove_inst(inst);
255        }
256        for &inst in &now {
257            func.append_inst(block, inst);
258        }
259    }
260    done
261}
262
263/// Whether nothing may be moved across that instruction.
264///
265/// See the module comment. The four answers are a call, a name the target does not have, a name the
266/// target has and the timing model does not cover, and an instruction carrying a frame rule.
267fn barrier(
268    func: &Func,
269    inst: Inst,
270    timing: &TimingInsts,
271    machine: &MachineInsts,
272    names: &Interner,
273) -> bool {
274    let name = names.resolve(func[inst].opcode.name());
275    machine.calls(name)
276        || !machine.has(name)
277        || timing.of(name).is_none_or(|timing| timing.unit == Unit::Fixed)
278        || func.cfi_after(inst).next().is_some()
279}
280
281/// Chooses an order for one run and appends it, saying whether there was anything to choose.
282#[allow(clippy::too_many_arguments)]
283fn order(
284    func: &Func,
285    run: &[Inst],
286    timing: &TimingInsts,
287    machine: &MachineInsts,
288    flags: &FlagInsts,
289    names: &Interner,
290    accurate: bool,
291    into: &mut Vec<Inst>,
292) -> bool {
293    if run.len() < 2 || run.len() > LONGEST {
294        into.extend_from_slice(run);
295        return false;
296    }
297    let nodes = graph(func, run, timing, machine, flags, names);
298    into.extend(list(&nodes, timing, accurate).into_iter().map(|at| run[at]));
299    true
300}
301
302/// One instruction of a run, and everything the choosing needs to know about it.
303#[derive(Debug)]
304struct Node {
305    /// What it costs, from the target's model.
306    timing: Timing,
307    /// The instructions that may not start before it, and how long each has to wait.
308    ///
309    /// The wait is how long the value takes where the edge is one instruction reading what another
310    /// wrote, and it is nothing where the edge is only about the two staying in order.
311    succs: Vec<(usize, u32)>,
312    /// How many instructions it may not start before, counted down as they are scheduled.
313    preds: usize,
314    /// The longest path from here to the end of the run, in cycles. Criterion one.
315    height: u32,
316    /// How many more registers are live after it than before. Criterion two.
317    ///
318    /// Within the run, so a register that is read here and read again in the next block counts as
319    /// dying here. Being wrong about that changes which of two instructions with the same critical
320    /// path goes first and nothing else, which is what a tiebreak is allowed to be wrong about.
321    growth: i32,
322}
323
324/// Builds the dependence graph of one run.
325fn graph(
326    func: &Func,
327    run: &[Inst],
328    timing: &TimingInsts,
329    machine: &MachineInsts,
330    flags: &FlagInsts,
331    names: &Interner,
332) -> Vec<Node> {
333    let costs: Vec<Timing> = run
334        .iter()
335        .map(|&inst| {
336            timing.of(names.resolve(func[inst].opcode.name())).expect("a barrier otherwise")
337        })
338        .collect();
339    let mut nodes: Vec<Node> = costs
340        .iter()
341        .map(|&timing| Node { timing, succs: Vec::new(), preds: 0, height: 0, growth: 0 })
342        .collect();
343
344    // The last instruction to write each register, and every instruction to read one since. The
345    // condition state is the same two questions with nowhere to keep the register's number, since
346    // it is not an operand on a machine that has one.
347    let mut wrote: HashMap<Place, usize> = HashMap::new();
348    let mut read: HashMap<Place, Vec<usize>> = HashMap::new();
349    let mut wrote_flags: Option<usize> = None;
350    let mut read_flags: Vec<usize> = Vec::new();
351    let mut touched: Option<usize> = None;
352
353    for (at, &inst) in run.iter().enumerate() {
354        let name = names.resolve(func[inst].opcode.name());
355        let bare = name.strip_prefix(flags.prefix).unwrap_or(name);
356
357        // Reads before writes, because an instruction whose destination is one of its own sources
358        // is on both lists and the write it does is not one its own read has to wait for.
359        for operand in &func[func[inst].operands] {
360            if operand.role == Role::Use {
361                let place = (operand.reg, operand.class);
362                if let Some(before) = wrote.get(&place) {
363                    edge(&mut nodes, *before, at, costs[*before].latency);
364                }
365                read.entry(place).or_default().push(at);
366            }
367        }
368        if flags.reads(bare).is_some() {
369            if let Some(before) = wrote_flags {
370                edge(&mut nodes, before, at, costs[before].latency);
371            }
372            read_flags.push(at);
373        }
374        for operand in &func[func[inst].operands] {
375            if operand.role.is_def() {
376                let place = (operand.reg, operand.class);
377                if let Some(before) = wrote.insert(place, at) {
378                    edge(&mut nodes, before, at, after(&costs, before));
379                }
380                for before in read.remove(&place).unwrap_or_default() {
381                    if before != at {
382                        edge(&mut nodes, before, at, 0);
383                    }
384                }
385            }
386        }
387        if (flags.writes)(bare) {
388            if let Some(before) = wrote_flags.replace(at) {
389                edge(&mut nodes, before, at, after(&costs, before));
390            }
391            for before in read_flags.drain(..) {
392                if before != at {
393                    edge(&mut nodes, before, at, 0);
394                }
395            }
396        }
397
398        // Memory and addresses, which are one chain. See the module comment.
399        if machine.touches_mem(name) || func[inst].mem.is_some() {
400            if let Some(before) = touched.replace(at) {
401                edge(&mut nodes, before, at, 0);
402            }
403        }
404    }
405
406    heights(&mut nodes);
407    growth(func, run, &mut nodes);
408    nodes
409}
410
411/// Says that the second instruction may not start until that many cycles after the first.
412///
413/// One edge per pair, keeping the longest wait. Two instructions are often joined for several
414/// reasons at once, and what the pair costs is the strongest of the reasons rather than the sum of
415/// them: a multiply whose result the next instruction reads and whose condition state it also
416/// overwrites is one edge of three cycles, not a three cycle edge and a one cycle edge. Keeping one
417/// edge per pair is also what makes criterion seven count instructions rather than reasons.
418fn edge(nodes: &mut [Node], from: usize, to: usize, wait: u32) {
419    if let Some(found) = nodes[from].succs.iter_mut().find(|(succ, _)| *succ == to) {
420        found.1 = found.1.max(wait);
421        return;
422    }
423    nodes[from].succs.push((to, wait));
424    nodes[to].preds += 1;
425}
426
427/// How long after one write of somewhere the next write of the same somewhere may start.
428///
429/// The two have to land in order, and an instruction that takes no time has landed by the time it
430/// has started, so this is a cycle for real work and nothing for the instructions that encode to
431/// nothing. The ones that encode to nothing are the reason it is worth asking: a machine function
432/// opens with an instruction per argument saying which register the argument is already in, each of
433/// them writes a register the real work then writes again, and charging a cycle for that held every
434/// first use of an argument one cycle behind where it could have been.
435fn after(costs: &[Timing], before: usize) -> u32 {
436    costs[before].latency.min(1)
437}
438
439/// The longest path from each instruction to the end of the run.
440///
441/// One pass backwards, which is all it takes because every edge goes from an earlier instruction to
442/// a later one: the graph is built by walking the run forwards and only ever putting an edge from
443/// something already seen to the instruction being looked at.
444fn heights(nodes: &mut [Node]) {
445    for at in (0..nodes.len()).rev() {
446        let mut height = nodes[at].timing.latency;
447        for index in 0..nodes[at].succs.len() {
448            let (succ, wait) = nodes[at].succs[index];
449            height = height.max(wait + nodes[succ].height);
450        }
451        nodes[at].height = height;
452    }
453}
454
455/// How many more registers are live after each instruction than before it.
456///
457/// A register a run reads for the last time is one whose value is not wanted afterwards, so the
458/// instruction that reads it gives a register back. One that writes a register takes one. The
459/// difference is what criterion two compares, and what it is really asking is whether an
460/// instruction is doing work or making something that will have to be kept until later.
461fn growth(func: &Func, run: &[Inst], nodes: &mut [Node]) {
462    let mut seen: HashSet<Place> = HashSet::new();
463    for (at, &inst) in run.iter().enumerate().rev() {
464        for operand in &func[func[inst].operands] {
465            if operand.role == Role::Use && seen.insert((operand.reg, operand.class)) {
466                nodes[at].growth -= 1;
467            }
468        }
469        for operand in &func[func[inst].operands] {
470            if operand.role.is_def() {
471                nodes[at].growth += 1;
472            }
473        }
474    }
475}
476
477/// The five numbers one instruction is chosen by, in the order they are compared.
478///
479/// Derived rather than written out, because the order the fields are in is the order section 38.1
480/// puts the criteria in and keeping the two the same is the point. Every field is one where smaller
481/// is better, so the one that sorts first is the one to schedule.
482#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
483struct Pick {
484    /// Criterion one, negated: the longest path to the end of the run, longest first.
485    path: i64,
486    /// Criterion two: how many registers it leaves live that were not, fewest first.
487    growth: i32,
488    /// Criterion six: whether it reads what was just scheduled, and so has to wait for it.
489    waits: bool,
490    /// Criterion seven, negated: how many instructions depend on it, most first.
491    users: i64,
492    /// Criterion eight: where it was in the input, earliest first.
493    at: usize,
494}
495
496/// Chooses an order, as positions into the run.
497fn list(nodes: &[Node], timing: &TimingInsts, accurate: bool) -> Vec<usize> {
498    let mut preds: Vec<usize> = nodes.iter().map(|node| node.preds).collect();
499    let mut when: Vec<u32> = vec![0; nodes.len()];
500    let mut ready: BTreeSet<usize> = (0..nodes.len()).filter(|&at| preds[at] == 0).collect();
501    let mut out: Vec<usize> = Vec::with_capacity(nodes.len());
502    let mut cycle = 0;
503    let mut used: HashMap<Unit, u32> = HashMap::new();
504    let mut issued = 0;
505    let mut last: Option<usize> = None;
506
507    while !ready.is_empty() {
508        let mut best: Option<Pick> = None;
509        for &at in ready.iter().take(READY) {
510            if when[at] > cycle || (accurate && !fits(nodes[at].timing.unit, &used, issued, timing))
511            {
512                continue;
513            }
514            let pick = Pick {
515                path: -i64::from(nodes[at].height),
516                growth: nodes[at].growth,
517                waits: last.is_some_and(|last| nodes[last].succs.iter().any(|&(to, _)| to == at)),
518                users: -(nodes[at].succs.len() as i64),
519                at,
520            };
521            if best.is_none_or(|best| pick < best) {
522                best = Some(pick);
523            }
524        }
525        let Some(best) = best else {
526            // Nothing can start this cycle, either because everything ready is still waiting on a
527            // value or because the units it wants are full. Both are answered by the next cycle,
528            // and jumping straight to the one something is ready in keeps a long latency from being
529            // walked over one cycle at a time.
530            let soonest = ready.iter().take(READY).map(|&at| when[at]).min().unwrap_or(cycle);
531            cycle = soonest.max(cycle + 1);
532            used.clear();
533            issued = 0;
534            continue;
535        };
536        let at = best.at;
537        ready.remove(&at);
538        out.push(at);
539        last = Some(at);
540        *used.entry(nodes[at].timing.unit).or_default() += 1;
541        issued += 1;
542        for index in 0..nodes[at].succs.len() {
543            let (succ, wait) = nodes[at].succs[index];
544            when[succ] = when[succ].max(cycle + wait);
545            preds[succ] -= 1;
546            if preds[succ] == 0 {
547                ready.insert(succ);
548            }
549        }
550    }
551    out
552}
553
554/// Whether the machine has room this cycle for an instruction on that unit.
555fn fits(unit: Unit, used: &HashMap<Unit, u32>, issued: u32, timing: &TimingInsts) -> bool {
556    issued < timing.width.max(1) && used.get(&unit).copied().unwrap_or(0) < timing.slots(unit)
557}
558
559#[cfg(test)]
560mod tests {
561    use rucc_mir::{Constraint, Mem, Opcode, Operand};
562    use rucc_target::PhysReg;
563    use rucc_target::x86_64::{
564        self, FLAGS, GPR, MACHINE, R8, R9, R10, RAX, RCX, RDI, RDX, RSI, TIMING, XMM,
565    };
566
567    use super::*;
568
569    /// A function with one block, and the names it was built with.
570    fn empty() -> (Interner, Func, Block) {
571        let mut names = Interner::new();
572        let mut func = Func::new(names.intern("f"));
573        let block = func.create_block();
574        (names, func, block)
575    }
576
577    /// The opcode of that name on this target.
578    fn op(names: &mut Interner, name: &str) -> Opcode {
579        Opcode::new(names.intern(&format!("{}{name}", MACHINE.prefix)))
580    }
581
582    /// A register the allocator has already handed out, which is all this pass ever sees.
583    fn reg(which: PhysReg) -> Reg {
584        Reg::physical(which)
585    }
586
587    /// Two address arithmetic writing one of its own sources, which is the shape this machine's
588    /// arithmetic has by the time the allocator has been through it.
589    fn alu(
590        func: &mut Func,
591        names: &mut Interner,
592        block: Block,
593        name: &str,
594        into: PhysReg,
595        from: PhysReg,
596    ) {
597        let opcode = op(names, name);
598        func.build(block, opcode)
599            .operand(Operand::write(reg(into), GPR).with(Constraint::Reuse(1)))
600            .uses(reg(into), GPR)
601            .uses(reg(from), GPR)
602            .finish();
603    }
604
605    /// The same, on the vector registers.
606    fn vector(
607        func: &mut Func,
608        names: &mut Interner,
609        block: Block,
610        name: &str,
611        into: PhysReg,
612        from: PhysReg,
613    ) {
614        let opcode = op(names, name);
615        func.build(block, opcode)
616            .operand(Operand::write(reg(into), XMM).with(Constraint::Reuse(1)))
617            .uses(reg(into), XMM)
618            .uses(reg(from), XMM)
619            .finish();
620    }
621
622    /// A move of one register into another.
623    fn mov(func: &mut Func, names: &mut Interner, block: Block, into: PhysReg, from: PhysReg) {
624        let opcode = op(names, "mov_rr_64");
625        func.build(block, opcode).def(reg(into), GPR).uses(reg(from), GPR).finish();
626    }
627
628    /// An eight byte read off that register.
629    fn load(func: &mut Func, names: &mut Interner, block: Block, into: PhysReg, base: PhysReg) {
630        let opcode = op(names, "mov_rm_64");
631        func.build(block, opcode)
632            .def(reg(into), GPR)
633            .mem(Mem::at(Operand::read(reg(base), GPR)))
634            .finish();
635    }
636
637    /// An instruction of that name with no operands at all, which is what a call, a fence and a
638    /// return are on this machine.
639    fn bare(func: &mut Func, names: &mut Interner, block: Block, name: &str) {
640        let opcode = op(names, name);
641        func.build(block, opcode).finish();
642    }
643
644    /// What every instruction in a block came to, as opcodes with the target's prefix taken off.
645    fn shape(func: &Func, names: &Interner, block: Block) -> Vec<String> {
646        func.insts(block)
647            .map(|inst| TIMING.bare(names.resolve(func[inst].opcode.name())).to_owned())
648            .collect()
649    }
650
651    /// The pass, with nothing pinned beyond the block's own last instruction.
652    fn schedule(func: &mut Func, names: &Interner) -> Scheduled {
653        insts(func, &TIMING, &MACHINE, &FLAGS, names, false, &HashSet::new())
654    }
655
656    /// A chain of three where only one order computes the right answer.
657    #[test]
658    fn a_block_already_in_the_only_order_it_has_comes_out_unchanged() {
659        let (mut names, mut func, block) = empty();
660        mov(&mut func, &mut names, block, RAX, RDX);
661        alu(&mut func, &mut names, block, "add_rr_64", RAX, RCX);
662        bare(&mut func, &mut names, block, "ret");
663
664        let done = schedule(&mut func, &names);
665        assert_eq!(done.moved, 0, "there was nothing else it could have written");
666        assert_eq!(shape(&func, &names, block), ["mov_rr_64", "add_rr_64", "ret"]);
667    }
668
669    /// The shape the whole pass is for: a multiply takes three cycles and the instruction that reads
670    /// it has to wait for all three, so work that was behind both of them is put in the middle.
671    #[test]
672    fn work_that_depends_on_nothing_moves_into_a_multiplys_latency() {
673        let (mut names, mut func, block) = empty();
674        alu(&mut func, &mut names, block, "imul_rr_64", RDI, RSI);
675        alu(&mut func, &mut names, block, "add_rr_64", RDI, RCX);
676        mov(&mut func, &mut names, block, RAX, RDX);
677        bare(&mut func, &mut names, block, "ret");
678
679        let done = schedule(&mut func, &names);
680        assert_eq!(done.runs, 1, "one run, since nothing in it is a barrier");
681        assert_eq!(
682            shape(&func, &names, block),
683            ["imul_rr_64", "mov_rr_64", "add_rr_64", "ret"],
684            "the move is doing a cycle of the three the addition was going to spend waiting"
685        );
686    }
687
688    /// A call, which is the barrier the module comment puts first. Without it the multiply below
689    /// would be hoisted over the call, since it has the longer path and nothing in its operands says
690    /// a call is in the way.
691    #[test]
692    fn nothing_crosses_a_call() {
693        let (mut names, mut func, block) = empty();
694        mov(&mut func, &mut names, block, RAX, RDX);
695        bare(&mut func, &mut names, block, "call");
696        alu(&mut func, &mut names, block, "imul_rr_64", RDI, RSI);
697        alu(&mut func, &mut names, block, "add_rr_64", RDI, RCX);
698        bare(&mut func, &mut names, block, "ret");
699
700        let done = schedule(&mut func, &names);
701        assert_eq!(done.moved, 0);
702        assert_eq!(
703            shape(&func, &names, block),
704            ["mov_rr_64", "call", "imul_rr_64", "add_rr_64", "ret"]
705        );
706    }
707
708    /// Two reads of memory. The second one starts a chain with a longer path than the first, so the
709    /// only thing keeping them in order is that they both touch memory.
710    #[test]
711    fn two_reads_of_memory_keep_the_order_they_arrived_in() {
712        let (mut names, mut func, block) = empty();
713        load(&mut func, &mut names, block, RAX, RDI);
714        load(&mut func, &mut names, block, RCX, RSI);
715        alu(&mut func, &mut names, block, "imul_rr_64", RCX, RDX);
716        bare(&mut func, &mut names, block, "ret");
717
718        let done = schedule(&mut func, &names);
719        assert_eq!(done.moved, 0);
720        assert_eq!(
721            shape(&func, &names, block),
722            ["mov_rm_64", "mov_rm_64", "imul_rr_64", "ret"],
723            "the read whose value nothing here wants stayed in front of the one that matters"
724        );
725    }
726
727    /// Two writes of one register, where the first one's value is never read. What decides the
728    /// register's contents afterwards is which of them ran last.
729    #[test]
730    fn two_writes_of_one_register_keep_the_order_they_arrived_in() {
731        let (mut names, mut func, block) = empty();
732        mov(&mut func, &mut names, block, RAX, RDX);
733        mov(&mut func, &mut names, block, RAX, RCX);
734        alu(&mut func, &mut names, block, "imul_rr_64", RAX, RSI);
735        bare(&mut func, &mut names, block, "ret");
736
737        let done = schedule(&mut func, &names);
738        assert_eq!(done.moved, 0);
739        assert_eq!(shape(&func, &names, block), ["mov_rr_64", "mov_rr_64", "imul_rr_64", "ret"]);
740    }
741
742    /// A write of a register something in front of it reads. No value passes between the two, and
743    /// the order between them is still the difference between right and wrong.
744    #[test]
745    fn a_write_stays_behind_the_read_of_what_the_register_held() {
746        let (mut names, mut func, block) = empty();
747        alu(&mut func, &mut names, block, "add_rr_64", RCX, RAX);
748        mov(&mut func, &mut names, block, RAX, RDX);
749        alu(&mut func, &mut names, block, "imul_rr_64", RAX, RSI);
750        bare(&mut func, &mut names, block, "ret");
751
752        let done = schedule(&mut func, &names);
753        assert_eq!(done.moved, 0);
754        assert_eq!(
755            shape(&func, &names, block),
756            ["add_rr_64", "mov_rr_64", "imul_rr_64", "ret"],
757            "the addition read what was in the register before the move put something else there"
758        );
759    }
760
761    /// The condition state, which is in no operand vector. The instruction that reads it has the
762    /// longer path of the two ready at the start, so if the target's answer about the flags were not
763    /// being used it would be scheduled first.
764    #[test]
765    fn the_instruction_that_reads_the_condition_state_stays_behind_the_comparison() {
766        let (mut names, mut func, block) = empty();
767        mov(&mut func, &mut names, block, RCX, RDX);
768        let cmp = op(&mut names, "cmp_rr_64");
769        func.build(block, cmp).uses(reg(RDI), GPR).uses(reg(RSI), GPR).finish();
770        let set = op(&mut names, "set_e");
771        func.build(block, set).def(reg(RAX), GPR).finish();
772        alu(&mut func, &mut names, block, "add_rr_64", RAX, R8);
773        bare(&mut func, &mut names, block, "ret");
774
775        schedule(&mut func, &names);
776        assert_eq!(
777            shape(&func, &names, block),
778            ["cmp_rr_64", "mov_rr_64", "set_e", "add_rr_64", "ret"],
779            "the move went into the cycle the set was waiting for the comparison in"
780        );
781    }
782
783    /// The block's own last instruction, which [`crate::layout`] needs where it is.
784    #[test]
785    fn the_last_instruction_of_a_block_never_moves() {
786        let (mut names, mut func, block) = empty();
787        mov(&mut func, &mut names, block, RAX, RDX);
788        mov(&mut func, &mut names, block, RCX, R8);
789        alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
790
791        let done = schedule(&mut func, &names);
792        assert_eq!(done.moved, 0);
793        assert_eq!(
794            shape(&func, &names, block),
795            ["mov_rr_64", "mov_rr_64", "imul_rr_64"],
796            "the multiply has the longest path and is last anyway"
797        );
798    }
799
800    /// What the caller pins, which is the comparison the layout is going to fuse with a branch.
801    #[test]
802    fn an_instruction_the_caller_pinned_never_moves() {
803        let build = |names: &mut Interner| {
804            let mut func = Func::new(names.intern("f"));
805            let block = func.create_block();
806            mov(&mut func, names, block, RAX, RDX);
807            mov(&mut func, names, block, RCX, R8);
808            alu(&mut func, names, block, "imul_rr_64", RSI, R9);
809            bare(&mut func, names, block, "ret");
810            (func, block)
811        };
812
813        let mut names = Interner::new();
814        let (mut loose, block) = build(&mut names);
815        schedule(&mut loose, &names);
816        assert_eq!(
817            shape(&loose, &names, block),
818            ["imul_rr_64", "mov_rr_64", "mov_rr_64", "ret"],
819            "with nothing pinned the multiply goes first, since it has the longest path"
820        );
821
822        let (mut held, block) = build(&mut names);
823        let second = held.insts(block).nth(1).expect("the second move");
824        insts(&mut held, &TIMING, &MACHINE, &FLAGS, &names, false, &HashSet::from([second]));
825        assert_eq!(
826            shape(&held, &names, block),
827            ["mov_rr_64", "mov_rr_64", "imul_rr_64", "ret"],
828            "pinning it splits the block into runs of one, and a run of one has one order"
829        );
830    }
831
832    /// A name the target does not have, which is the barrier that keeps a rule set growing an opcode
833    /// from quietly growing a wrong schedule.
834    #[test]
835    fn a_name_this_target_does_not_have_is_a_barrier() {
836        let (mut names, mut func, block) = empty();
837        mov(&mut func, &mut names, block, RAX, RDX);
838        bare(&mut func, &mut names, block, "not_an_instruction_this_machine_has");
839        alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
840        bare(&mut func, &mut names, block, "ret");
841
842        let done = schedule(&mut func, &names);
843        assert_eq!(done.moved, 0);
844        assert_eq!(
845            shape(&func, &names, block),
846            ["mov_rr_64", "not_an_instruction_this_machine_has", "imul_rr_64", "ret"]
847        );
848    }
849
850    /// A trap, which the target has and the timing model deliberately does not describe.
851    #[test]
852    fn an_instruction_the_model_does_not_describe_is_a_barrier() {
853        let (mut names, mut func, block) = empty();
854        mov(&mut func, &mut names, block, RAX, RDX);
855        bare(&mut func, &mut names, block, "ud2");
856        alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
857        bare(&mut func, &mut names, block, "ret");
858
859        assert_eq!(TIMING.of("x64.ud2").expect("described").unit, Unit::Fixed);
860        let done = schedule(&mut func, &names);
861        assert_eq!(done.moved, 0);
862        assert_eq!(shape(&func, &names, block), ["mov_rr_64", "ud2", "imul_rr_64", "ret"]);
863    }
864
865    /// The property that holds whatever the model says, since the model chooses among orders and
866    /// does not choose what is in one.
867    #[test]
868    fn what_comes_out_is_the_instructions_that_went_in_and_no_others() {
869        let (mut names, mut func, block) = empty();
870        alu(&mut func, &mut names, block, "imul_rr_64", RDI, RSI);
871        mov(&mut func, &mut names, block, RAX, RDX);
872        load(&mut func, &mut names, block, RCX, R8);
873        alu(&mut func, &mut names, block, "add_rr_64", RAX, RCX);
874        alu(&mut func, &mut names, block, "sub_rr_64", RDX, R9);
875        mov(&mut func, &mut names, block, R10, RDI);
876        alu(&mut func, &mut names, block, "imul_rr_64", R10, RAX);
877        alu(&mut func, &mut names, block, "add_rr_64", R10, RDX);
878        bare(&mut func, &mut names, block, "ret");
879        let mut was: Vec<Inst> = func.insts(block).collect();
880
881        schedule(&mut func, &names);
882        let mut now: Vec<Inst> = func.insts(block).collect();
883        assert_eq!(now.len(), was.len(), "nothing was added or dropped");
884        was.sort_unstable();
885        now.sort_unstable();
886        assert_eq!(now, was, "the same instructions, in some order");
887    }
888
889    /// The output is a function of the input. Two hash maps in one process do not agree about the
890    /// order they hand their contents back in, so anything in here that walked one would show up
891    /// here rather than as a program that comes out differently on somebody else's machine.
892    #[test]
893    fn the_same_block_twice_gives_the_same_order_twice() {
894        let build = |names: &mut Interner| {
895            let mut func = Func::new(names.intern("f"));
896            let block = func.create_block();
897            alu(&mut func, names, block, "imul_rr_64", RDI, RSI);
898            mov(&mut func, names, block, RAX, RDX);
899            load(&mut func, names, block, RCX, R8);
900            alu(&mut func, names, block, "add_rr_64", RAX, RCX);
901            alu(&mut func, names, block, "sub_rr_64", RDX, R9);
902            mov(&mut func, names, block, R10, RDI);
903            alu(&mut func, names, block, "imul_rr_64", R10, RAX);
904            bare(&mut func, names, block, "ret");
905            (func, block)
906        };
907
908        let mut names = Interner::new();
909        let (mut first, one) = build(&mut names);
910        let (mut second, two) = build(&mut names);
911        schedule(&mut first, &names);
912        schedule(&mut second, &names);
913        assert_eq!(shape(&first, &names, one), shape(&second, &names, two));
914    }
915
916    /// Criterion two. Both of these are ready at the start and both are the same distance from the
917    /// end, and the one that hands a register back goes first.
918    #[test]
919    fn a_constant_put_in_a_register_is_not_hoisted_over_work_that_hands_one_back() {
920        let (mut names, mut func, block) = empty();
921        let load_imm = op(&mut names, "mov_ri_64");
922        func.build(block, load_imm).def(reg(RCX), GPR).imm(5).finish();
923        alu(&mut func, &mut names, block, "add_rr_64", RAX, RDX);
924        alu(&mut func, &mut names, block, "add_rr_64", RAX, RCX);
925        bare(&mut func, &mut names, block, "ret");
926
927        schedule(&mut func, &names);
928        assert_eq!(
929            shape(&func, &names, block),
930            ["add_rr_64", "mov_ri_64", "add_rr_64", "ret"],
931            "the constant is loaded as early as necessary and no earlier"
932        );
933    }
934
935    /// What [`TimingInsts::accurate`] is for. Three vector additions want the two floating point
936    /// units, and a model worth believing about its units holds the third back and fills the cycle
937    /// with the move instead.
938    #[test]
939    fn a_model_worth_believing_about_its_units_fills_a_full_cycle_with_other_work() {
940        let build = |names: &mut Interner| {
941            let mut func = Func::new(names.intern("f"));
942            let block = func.create_block();
943            vector(&mut func, names, block, "addsd_rr", x86_64::xmm(0), x86_64::xmm(1));
944            vector(&mut func, names, block, "addsd_rr", x86_64::xmm(2), x86_64::xmm(3));
945            vector(&mut func, names, block, "addsd_rr", x86_64::xmm(4), x86_64::xmm(5));
946            mov(&mut func, names, block, RAX, RDX);
947            bare(&mut func, names, block, "ret");
948            (func, block)
949        };
950
951        assert_eq!(TIMING.slots(Unit::Float), 2, "the machine this model describes has two");
952
953        let mut names = Interner::new();
954        let (mut loose, block) = build(&mut names);
955        insts(&mut loose, &TIMING, &MACHINE, &FLAGS, &names, false, &HashSet::new());
956        assert_eq!(
957            shape(&loose, &names, block),
958            ["addsd_rr", "addsd_rr", "addsd_rr", "mov_rr_64", "ret"],
959            "without the units the three additions are the same instruction three times over"
960        );
961
962        let (mut tight, block) = build(&mut names);
963        insts(&mut tight, &TIMING, &MACHINE, &FLAGS, &names, true, &HashSet::new());
964        assert_eq!(
965            shape(&tight, &names, block),
966            ["addsd_rr", "addsd_rr", "mov_rr_64", "addsd_rr", "ret"],
967            "the third addition has nowhere to go this cycle and the move has"
968        );
969    }
970
971    /// The bound, and the same block below it as the control. A run of a few thousand machine
972    /// instructions is a generated table rather than something anybody is waiting on the schedule
973    /// of, and building the graph for one is quadratic in the worst case.
974    ///
975    /// The moves all write the same register, so they are a chain that has to run in the order it
976    /// is in and the first of them is further from the end of the run than a three cycle multiply
977    /// is. Below the bound that is what decides the order. Above it nothing decides anything.
978    #[test]
979    fn a_run_longer_than_the_bound_is_left_alone() {
980        let build = |names: &mut Interner, moves: usize| {
981            let mut func = Func::new(names.intern("f"));
982            let block = func.create_block();
983            alu(&mut func, names, block, "imul_rr_64", RSI, R9);
984            for _ in 0..moves {
985                mov(&mut func, names, block, RAX, RDX);
986            }
987            bare(&mut func, names, block, "ret");
988            (func, block)
989        };
990
991        let mut names = Interner::new();
992        let (mut short, block) = build(&mut names, 8);
993        let done = schedule(&mut short, &names);
994        assert!(done.moved > 0, "below the bound a run is looked at");
995        assert_eq!(
996            shape(&short, &names, block).first().map(String::as_str),
997            Some("mov_rr_64"),
998            "the chain of moves is the long way round and starts first"
999        );
1000
1001        let (mut long, block) = build(&mut names, LONGEST);
1002        let done = schedule(&mut long, &names);
1003        assert_eq!(done.moved, 0, "above it the run is written back exactly as it arrived");
1004        assert_eq!(shape(&long, &names, block).first().map(String::as_str), Some("imul_rr_64"));
1005    }
1006
1007    /// A block too short to have anything to choose, which is the one case the pass skips outright.
1008    #[test]
1009    fn a_block_of_two_instructions_is_not_looked_at() {
1010        let (mut names, mut func, block) = empty();
1011        alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
1012        bare(&mut func, &mut names, block, "ret");
1013
1014        let done = schedule(&mut func, &names);
1015        assert_eq!(done, Scheduled::default());
1016        assert_eq!(shape(&func, &names, block), ["imul_rr_64", "ret"]);
1017    }
1018
1019    /// A shift by a variable amount, which the machine takes out of one particular register and
1020    /// this target's description names as an operand with that register fixed. The whole of this
1021    /// pass reads operand vectors, so an instruction whose description left a register it touches
1022    /// out of one would be reordered around a write of it. This is the check that it does not.
1023    #[test]
1024    fn a_shift_by_a_variable_amount_stays_behind_the_write_of_the_register_it_counts() {
1025        let (mut names, mut func, block) = empty();
1026        mov(&mut func, &mut names, block, RCX, R8);
1027        let shift = op(&mut names, "shl_rcl_64");
1028        func.build(block, shift)
1029            .operand(Operand::write(reg(RAX), GPR).with(Constraint::Reuse(1)))
1030            .uses(reg(RAX), GPR)
1031            .uses(reg(RCX), GPR)
1032            .finish();
1033        alu(&mut func, &mut names, block, "imul_rr_64", RAX, RDX);
1034        bare(&mut func, &mut names, block, "ret");
1035
1036        let done = schedule(&mut func, &names);
1037        assert_eq!(done.moved, 0);
1038        assert_eq!(shape(&func, &names, block), ["mov_rr_64", "shl_rcl_64", "imul_rr_64", "ret"]);
1039    }
1040
1041    /// A divide, which reads and writes two particular registers and names all four of them. It is
1042    /// twenty six cycles from the end of this run and the move in front of it is one, so the only
1043    /// thing keeping it where it is is that it said it writes the register the move writes.
1044    #[test]
1045    fn a_divide_names_both_of_the_registers_the_machine_makes_it_use() {
1046        let (mut names, mut func, block) = empty();
1047        mov(&mut func, &mut names, block, RDX, R8);
1048        let divide = op(&mut names, "idiv_quo_64");
1049        func.build(block, divide)
1050            .operand(Operand::write(reg(RAX), GPR).with(Constraint::Fixed(RAX)))
1051            .operand(Operand::write_early(reg(RDX), GPR).with(Constraint::Fixed(RDX)))
1052            .operand(Operand::read(reg(RAX), GPR).with(Constraint::Fixed(RAX)))
1053            .uses(reg(RSI), GPR)
1054            .finish();
1055        bare(&mut func, &mut names, block, "ret");
1056
1057        assert!(TIMING.of("x64.idiv_quo_64").expect("described").latency > 1);
1058        let done = schedule(&mut func, &names);
1059        assert_eq!(done.moved, 0);
1060        assert_eq!(shape(&func, &names, block), ["mov_rr_64", "idiv_quo_64", "ret"]);
1061    }
1062
1063    /// Every unit the model has, reached through an instruction that is on it, since a unit nothing
1064    /// can get a slot on is a scheduler that does not finish.
1065    #[test]
1066    fn every_unit_a_run_can_ask_for_has_at_least_one_of_it() {
1067        for &unit in Unit::ALL {
1068            assert!(TIMING.slots(unit) >= 1, "{unit:?} has none of it");
1069        }
1070    }
1071
1072    /// Two files that each number their registers from zero. The move and the addition here both
1073    /// write the register numbered nothing, and they are not writing the same register: one is the
1074    /// first integer register and the other is the first vector register. See [`Place`].
1075    #[test]
1076    fn the_first_register_of_each_file_is_not_the_same_register() {
1077        let (mut names, mut func, block) = empty();
1078        mov(&mut func, &mut names, block, RAX, RDX);
1079        vector(&mut func, &mut names, block, "addsd_rr", x86_64::xmm(0), x86_64::xmm(1));
1080        bare(&mut func, &mut names, block, "ret");
1081
1082        assert_eq!(reg(RAX), reg(x86_64::xmm(0)), "and a register on its own does not say which");
1083        schedule(&mut func, &names);
1084        assert_eq!(
1085            shape(&func, &names, block),
1086            ["addsd_rr", "mov_rr_64", "ret"],
1087            "the addition is four cycles from the end and the move is one, and nothing joins them"
1088        );
1089    }
1090}