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}