Skip to main content

rucc_regalloc/
lib.rs

1//! Both register allocators and the allocation checker.
2//!
3//! Design: `spec/10-backend.md`. Layer rank 11, see `spec/18-package-layout.md`.
4//!
5//! # Status
6//!
7//! Liveness is here, which is the question both allocators ask first: [`order`] lays a function out
8//! in the line the encoder will emit it in, and [`live`] says where in that line each value is
9//! wanted. So is [`moves`], which puts the moves an edge turns into in an order they can be made in
10//! one at a time. The single pass allocator's decision is in [`assign`]: where every value of a
11//! function goes, in one linear scan, which is what `-O0` asks for. The rewrite that makes that
12//! decision true in the function is in [`rewrite`], with what each instruction needs around it for
13//! the machine to accept the places worked out in [`legalize`], and [`run`] is the two of them
14//! together, which is the whole of the `-O0` allocator. [`check`] reads an assignment back and says
15//! whether it is one the machine can run, which [`run`] asserts on in debug and CI builds and which
16//! the backtracking allocator is held to the same way. [`trace`] asks the other half of the
17//! question, which is whether the rewrite wrote that decision down without losing a value on the
18//! way: it follows every value from the instruction that wrote it to the instructions that read it,
19//! through the moves, and [`run`] asserts on it in the same builds.
20//!
21//! The allocator the optimizer uses is in [`backtrack`]. [`pressure`] counts, at every point, the
22//! values that want a register against the registers there are, and [`spill`] reads that to pick
23//! which values go to memory before [`backtrack`] places the rest.
24//!
25//! Every crate in the workspace is published, and publishing implies a promise. This one is
26//! tier 3: its Rust API is explicitly unstable and will change without a major version bump.
27//! Depend on the `rucc` binary's behaviour, not on this.
28
29#![doc(html_root_url = "https://docs.rs/rucc-regalloc/0.24.5")]
30
31pub mod assign;
32pub mod backtrack;
33pub mod check;
34pub mod legalize;
35pub mod live;
36pub mod moves;
37pub mod order;
38pub mod pressure;
39pub mod rewrite;
40pub mod spill;
41pub mod trace;
42
43/// What allocating a function produced.
44///
45/// The moves are handed back rather than written into the function because a move is an
46/// instruction and an instruction belongs to a target, which `spec/10-backend.md` section 10.8
47/// says this crate holds nothing of. The consumer turns each one into whatever its target moves a
48/// register with.
49#[derive(Debug, Clone)]
50pub struct Allocation {
51    /// Where every value of the function went, which is what the frame layout reads.
52    pub assignment: assign::Assignment,
53    /// The moves the places do not already make true, in the order they have to be made in.
54    pub edits: Vec<rewrite::Edit>,
55    /// The line the function was laid out in while it was allocated, which the liveness below is
56    /// counted along.
57    pub order: order::Order,
58    /// Where every value was live.
59    ///
60    /// Handed back rather than dropped because the stack slot allocator shares one run of bytes
61    /// between two things that are never both wanted, and the only liveness that knows where a
62    /// spilled value is wanted is the one the spilling was decided from. Working a second one out
63    /// afterwards would cost a pass and would be free to disagree with this one.
64    /// `spec/optimizer/36-lowering-and-isel.md` section 36.7 asks for the one answer.
65    pub live: live::Live,
66}
67
68/// Allocates registers for a function the way `-O0` asks for, rewriting it as it goes.
69///
70/// This is the shape `spec/10-backend.md` section 10.4 gives an allocator: a function and the
71/// registers it may use in, an assignment and the moves that make it true out. The backtracking
72/// allocator will answer the same question the same way.
73///
74/// # Panics
75///
76/// Panics on a function the caller was told not to hand it, which is one with a critical edge or
77/// one whose entry block has parameters. See [`rewrite::rewrite`].
78///
79/// It also panics on an assignment [`check`] finds a problem with, which is a bug in this crate or
80/// in whatever produced the function rather than anything the caller did. `spec/10-backend.md`
81/// section 10.4 asks for that check in debug and CI builds, and it runs before the rewrite because
82/// the assignment is the decision and the rewrite only writes it down.
83///
84/// It panics on a rewrite [`trace`] finds a value missing from as well. That one runs afterwards,
85/// since a transcription can only be read once it has been made, and it is the check
86/// `spec/optimizer/39-register-allocation.md` section 39.6 asks for.
87///
88/// `verify` is what turns both of those on in a build that has assertions compiled out. A debug
89/// build runs them whatever it says, since that is where a broken pass should be caught, and a
90/// release build runs them when the caller asks, which is what `-Zverify-each` is for and what
91/// section 10.4 means by a CI build. It is a parameter rather than a `cfg!` because the thing
92/// worth catching is a pass that writes a function nothing defines a register in, the gate
93/// compiles release, and a check the gate never runs is a check that finds the bug after the merge
94/// rather than on the pull request. tamnd/rucc#1411.
95///
96/// `called` is what to call the function in that message. It is passed in rather than read off the
97/// function because the name there is a symbol and resolving one wants the interner, which this
98/// crate has no reason to be handed otherwise. Without it the message is a pair of register numbers
99/// and nothing that says where, and finding the function it was about in a file the size of the
100/// SQLite amalgamation means bisecting by hand.
101pub fn run(func: &mut rucc_mir::Func, env: &assign::Env, called: &str, verify: bool) -> Allocation {
102    run_with(func, env, called, verify, Allocator::Single)
103}
104
105/// Which of the two allocators decides where the values go.
106///
107/// The rewrite, the checks and the moves are the same for both, since each hands back an
108/// [`assign::Assignment`] and nothing after the decision asks which one made it.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum Allocator {
111    /// The single pass of [`assign`], which is what `-O0` asks for.
112    #[default]
113    Single,
114    /// The allocator of [`backtrack`], which weighs what each value costs to keep in memory.
115    Backtracking,
116}
117
118/// Allocates registers for a function with the allocator asked for, rewriting it as it goes.
119///
120/// # Panics
121///
122/// As [`run`].
123pub fn run_with(
124    func: &mut rucc_mir::Func,
125    env: &assign::Env,
126    called: &str,
127    verify: bool,
128    allocator: Allocator,
129) -> Allocation {
130    let order = order::Order::of(func);
131    let live = live::Live::of(func, &order);
132    let assignment = decide(func, &order, &live, env, allocator);
133    write(func, assignment, env, order, live, called, verify)
134}
135
136/// Allocates registers with `wide` where that needs none of the scratch registers it leaves out,
137/// and with `env` where it does.
138///
139/// `wide` is `env` with some of what `env` holds back handed out instead. Holding a register back
140/// costs every function that never reads a value off the stack, which is most of them, and on
141/// x86-64 the two held back are two of the nine a call may destroy. A function short of those
142/// reaches for one the callee has to save, which is a push and a pop for a register the scratch
143/// pair could have been. So the allocator is asked with the registers handed out first, and what it
144/// decides is kept if [`rewrite::fits`] says the rewrite will never reach for a scratch register
145/// the class does not have. When it would, the function is allocated again the way it always was.
146///
147/// The answer is the same shape either way, and nothing after it has to ask which one it was. A
148/// caller that writes code of its own into a scratch register after allocation has to do it where
149/// no value is live, which a prologue and a return are.
150///
151/// # Panics
152///
153/// As [`run`].
154pub fn run_either(
155    func: &mut rucc_mir::Func,
156    wide: &assign::Env,
157    env: &assign::Env,
158    called: &str,
159    verify: bool,
160    allocator: Allocator,
161) -> Allocation {
162    let order = order::Order::of(func);
163    let live = live::Live::of(func, &order);
164    let tried = decide(func, &order, &live, wide, allocator);
165    if rewrite::fits(func, &tried, wide) {
166        return write(func, tried, wide, order, live, called, verify);
167    }
168    let assignment = decide(func, &order, &live, env, allocator);
169    write(func, assignment, env, order, live, called, verify)
170}
171
172/// Where every value goes, with the allocator asked for.
173fn decide(
174    func: &rucc_mir::Func,
175    order: &order::Order,
176    live: &live::Live,
177    env: &assign::Env,
178    allocator: Allocator,
179) -> assign::Assignment {
180    match allocator {
181        Allocator::Single => assign::assign(func, order, live, env),
182        Allocator::Backtracking => backtrack::assign(func, order, live, env),
183    }
184}
185
186/// Makes a decision true in the function, checking it on the way in and the rewrite on the way out
187/// in a build that asks for that.
188fn write(
189    func: &mut rucc_mir::Func,
190    mut assignment: assign::Assignment,
191    env: &assign::Env,
192    order: order::Order,
193    live: live::Live,
194    called: &str,
195    verify: bool,
196) -> Allocation {
197    let checking = verify || cfg!(debug_assertions);
198    // An answer that went over the second source of an instruction that reads its sources either
199    // way round. Swapping them makes it an ordinary reuse of the first, so the checker, the trace
200    // and the rewrite read it as one. Liveness does not care which way round two uses are.
201    for &inst in assignment.commuted() {
202        let list = func[inst].operands;
203        func[list].swap(1, 2);
204    }
205    if checking {
206        let problems = check::check(func, &order, &live, &assignment);
207        assert!(problems.is_empty(), "in '{called}': {}", check::report(&problems));
208    }
209    // What the rewrite is about to lose, taken while it is still there. Only in a build that is
210    // going to read it, since the snapshot is a copy of every operand list in the function.
211    let shape = checking.then(|| trace::shape(func));
212    let edits = rewrite::rewrite(func, &mut assignment, env);
213    if let Some(shape) = shape {
214        let faults = trace::trace(func, &shape, &assignment, &edits);
215        assert!(faults.is_empty(), "in '{called}': {}", trace::report(&faults));
216    }
217    Allocation { assignment, edits, order, live }
218}
219
220/// The milestone in `spec/17-milestones.md` that fills this crate in.
221pub const MILESTONE: &str = "M3";
222
223#[cfg(test)]
224mod tests {
225    use rucc_base::Interner;
226    use rucc_mir::{BlockCall, Func, Opcode, Operand, Reg, Weight};
227    use rucc_target::x86_64::{GPR, RAX, RCX, RDX, SYSV};
228
229    use super::*;
230
231    #[test]
232    fn milestone_is_recorded() {
233        assert!(MILESTONE.starts_with('M'));
234    }
235
236    #[test]
237    fn allocating_a_function_places_every_value_and_hands_back_the_moves_it_needs() {
238        let mut names = Interner::new();
239        let mut func = Func::new(names.intern("f"));
240        let opcode = Opcode::new(names.intern("x64.nop"));
241        let block = func.create_block();
242        let first = func.new_vreg(GPR);
243        let second = func.new_vreg(GPR);
244        let third = func.new_vreg(GPR);
245        func.build(block, opcode).def(first, GPR).finish();
246        func.build(block, opcode).def(second, GPR).finish();
247        func.build(block, opcode).def(third, GPR).finish();
248        func.build(block, opcode).uses(first, GPR).uses(second, GPR).uses(third, GPR).finish();
249
250        // Two registers to hand out and three values that are all wanted at once, so one of them
251        // goes to the stack and the instruction that reads it gets a reload. This is also where
252        // the checker runs, since a debug build asserts on what it says.
253        let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
254        let allocation = run(&mut func, &env, "test", true);
255
256        assert_eq!(allocation.assignment.spilled(), 1);
257        assert_eq!(allocation.edits.len(), 2);
258    }
259
260    /// Three values wanted at once, which is the function the test above spills one of.
261    fn three_at_once(names: &mut Interner) -> (Func, [Reg; 3]) {
262        let mut func = Func::new(names.intern("f"));
263        let opcode = Opcode::new(names.intern("x64.nop"));
264        let block = func.create_block();
265        let values = [(); 3].map(|()| func.new_vreg(GPR));
266        for value in values {
267            func.build(block, opcode).def(value, GPR).finish();
268        }
269        func.build(block, opcode)
270            .uses(values[0], GPR)
271            .uses(values[1], GPR)
272            .uses(values[2], GPR)
273            .finish();
274        (func, values)
275    }
276
277    #[test]
278    fn a_function_that_needs_no_scratch_register_is_given_the_scratch_registers() {
279        let mut names = Interner::new();
280        let (mut func, values) = three_at_once(&mut names);
281
282        // Two to hand out and one held back, or three to hand out and none held back. Nothing
283        // spills with three, so the third is the one held back and no move is wanted anywhere.
284        let wide = assign::Env::new().with(GPR, &SYSV.int_order[..3], &[]);
285        let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..4]);
286        let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
287
288        assert_eq!(allocation.assignment.spilled(), 0);
289        assert!(allocation.edits.is_empty());
290        let mut places: Vec<_> =
291            values.iter().filter_map(|&value| allocation.assignment.place(value)).collect();
292        places.sort_by_key(|place| match place {
293            assign::Place::Reg(reg) => reg.number(),
294            assign::Place::Slot(_) => u8::MAX,
295        });
296        let regs = [RAX, RCX, RDX].map(assign::Place::Reg);
297        assert_eq!(places, regs);
298    }
299
300    #[test]
301    fn a_function_that_spills_is_allocated_again_with_the_scratch_registers_held_back() {
302        let mut names = Interner::new();
303        let (mut func, _) = three_at_once(&mut names);
304
305        // Two to hand out either way. The wide answer spills a value it has nothing to read back
306        // into, so the function is allocated again with the scratch registers held back.
307        let wide = assign::Env::new().with(GPR, &SYSV.int_order[..2], &[]);
308        let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..4]);
309        let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
310
311        assert_eq!(allocation.assignment.spilled(), 1);
312        assert_eq!(allocation.edits.len(), 2);
313    }
314
315    #[test]
316    fn two_values_swapping_on_an_edge_are_allocated_with_the_scratch_registers_held_back() {
317        let mut names = Interner::new();
318        let mut func = Func::new(names.intern("f"));
319        let opcode = Opcode::new(names.intern("x64.nop"));
320        let head = func.create_block();
321        let body = func.create_block();
322        let first = func.new_vreg(GPR);
323        let second = func.new_vreg(GPR);
324        func.build(head, opcode).def(first, GPR).finish();
325        func.build(head, opcode).def(second, GPR).finish();
326        let left = func.append_param(body, GPR);
327        let right = func.append_param(body, GPR);
328        *func.succs_mut(head) = vec![BlockCall::with(body, vec![first, second])];
329        func.build(body, opcode).uses(left, GPR).uses(right, GPR).finish();
330        *func.succs_mut(body) = vec![BlockCall::with(body, vec![right, left])];
331
332        // Nothing spills, but the loop hands the two values back the other way round, which takes a
333        // third register for one of them while the other moves.
334        let wide = assign::Env::new().with(GPR, &SYSV.int_order[..2], &[]);
335        let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..4]);
336        let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
337
338        assert_eq!(allocation.assignment.spilled(), 0);
339        let through = assign::Place::Reg(RDX);
340        assert!(allocation.edits.iter().any(|edit| edit.mov.to == through), "{allocation:?}");
341    }
342
343    #[test]
344    fn a_value_carried_round_a_loop_is_allocated_with_the_scratch_registers_given_out() {
345        let mut names = Interner::new();
346        let mut func = Func::new(names.intern("f"));
347        let opcode = Opcode::new(names.intern("x64.nop"));
348        let head = func.create_block();
349        let body = func.create_block();
350        let first = func.new_vreg(GPR);
351        func.build(head, opcode).def(first, GPR).finish();
352        let carried = func.append_param(body, GPR);
353        *func.succs_mut(head) = vec![BlockCall::with(body, vec![first])];
354        let next = func.new_vreg(GPR);
355        func.build(body, opcode).def(next, GPR).uses(carried, GPR).finish();
356        *func.succs_mut(body) = vec![BlockCall::with(body, vec![next])];
357
358        // One value on each edge goes round in no cycle, so the answer with nothing held back is
359        // the one written, and writing the edge has no scratch register to ask for.
360        let wide = assign::Env::new().with(GPR, &SYSV.int_order[..2], &[]);
361        let env = assign::Env::new().with(GPR, &SYSV.int_order[..1], &SYSV.int_order[1..3]);
362        let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
363
364        assert_eq!(allocation.assignment.spilled(), 0);
365        for value in [first, carried, next] {
366            let place = allocation.assignment.place(value);
367            assert!(matches!(place, Some(assign::Place::Reg(_))), "{allocation:?}");
368        }
369    }
370
371    #[test]
372    fn a_value_a_loop_reads_is_put_away_around_the_call_the_loop_seldom_makes() {
373        let mut names = Interner::new();
374        let mut func = Func::new(names.intern("f"));
375        let opcode = Opcode::new(names.intern("x64.nop"));
376        let [entry, head, cold, skip, latch, back, out] = [(); 7].map(|()| func.create_block());
377        let step = func.new_vreg(GPR);
378        func.build(entry, opcode).def(step, GPR).finish();
379        *func.succs_mut(entry) = vec![BlockCall::to(head)];
380        func.build(head, opcode).uses(step, GPR).finish();
381        *func.succs_mut(head) = vec![BlockCall::to(cold), BlockCall::to(skip)];
382        // A call, as far as the allocator can tell: both registers it hands out are destroyed.
383        let call = func
384            .build(cold, opcode)
385            .operand(Operand::write(Reg::physical(RAX), GPR))
386            .operand(Operand::write(Reg::physical(RCX), GPR))
387            .finish();
388        *func.succs_mut(cold) = vec![BlockCall::to(latch)];
389        *func.succs_mut(skip) = vec![BlockCall::to(latch)];
390        func.build(latch, opcode).uses(step, GPR).finish();
391        *func.succs_mut(latch) = vec![BlockCall::to(back), BlockCall::to(out)];
392        *func.succs_mut(back) = vec![BlockCall::to(head)];
393        func.build(out, opcode).uses(step, GPR).finish();
394        for (block, often) in [(head, 100), (skip, 99), (latch, 100), (back, 99)] {
395            func.set_weight(block, Weight::parts(often * Weight::SCALE));
396        }
397
398        // Two registers and the call destroys both, so the only other answer is the stack and a
399        // load in every turn. The trace runs here as well, and it is what says the value read back
400        // behind the call is the one put away in front of it.
401        let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
402        let allocation = run_with(&mut func, &env, "test", true, Allocator::Backtracking);
403
404        let assignment = &allocation.assignment;
405        assert_eq!(assignment.spilled(), 0);
406        let Some(assign::Place::Reg(at)) = assignment.place(step) else {
407            panic!("the value went to the stack");
408        };
409        let saves = assignment.saves();
410        assert_eq!(saves.len(), 1);
411        assert_eq!((saves[0].reg, saves[0].inst), (step, call));
412        let slot = assign::Place::Slot(saves[0].slot);
413        let here = |edit: &&rewrite::Edit| match edit.at {
414            rewrite::At::Before(inst) | rewrite::At::After(inst) => inst == call,
415            _ => false,
416        };
417        let around: Vec<_> = allocation
418            .edits
419            .iter()
420            .filter(here)
421            .map(|edit| (edit.at, edit.mov.to, edit.mov.from))
422            .collect();
423        assert_eq!(
424            around,
425            [
426                (rewrite::At::Before(call), slot, assign::Place::Reg(at)),
427                (rewrite::At::After(call), assign::Place::Reg(at), slot),
428            ]
429        );
430    }
431}