Skip to main content

rucc_codegen/
reload.rs

1//! Taking out a reload that reads back the slot the instruction in front of it has just filled.
2//!
3//! Design: `spec/optimizer/37-machine-level-optimization.md` sections 37.4 and 37.6, the group of
4//! passes that run after allocation and clean up what the allocator could not.
5//!
6//! The allocator decides one value at a time. It writes a value out when the range it was given a
7//! register for ends, and it reads a value back in front of the instruction that wants it, and
8//! neither decision looks at the other. So a value written by one instruction and wanted by the
9//! next comes out as a store and then the load of the same slot on the very next line:
10//!
11//! ```text
12//!   movq %r10, 16(%rsp)
13//!   movq 16(%rsp), %r10
14//! ```
15//!
16//! The load reads a word the store has just written, into the register the store read it out of,
17//! and nothing stands between the two, so the register already holds what the load would put in
18//! it. It is a memory access that cannot change anything, on the two instructions of the pair that
19//! are the expensive one. That is what this takes out.
20//!
21//! # Why it is not a rule over the instructions
22//!
23//! A store followed by a load of the same address is not on its own a dead load. The same pair of
24//! instructions is what a write to a local variable and a read of it back look like, and when that
25//! variable is `volatile` the read is one the program insisted on and the standard says happens.
26//! Machine IR does not carry that word, and by the time the pass runs it could not: a volatile
27//! access and an ordinary one are the same instruction with the same operands.
28//!
29//! So this does not look for the pattern. [`crate::finish`] records which instruction each of the
30//! allocator's moves became, and this pass only ever takes out one of those. A spill slot belongs
31//! to the allocator, nothing else reads it, and no part of the program said anything about it,
32//! which is what makes removing a read of one safe when removing a read of a variable is not.
33//!
34//! # Why after the whole allocator rather than inside it
35//!
36//! The two edits are decided in different places for different reasons, so neither of the two
37//! decisions is wrong on its own and there is no one place inside the allocator that sees the
38//! pair. Adjacency is also not a property either decision has: whether anything ends up between
39//! them is settled by [`crate::finish`] writing every edit into the function, which is after the
40//! allocator has finished. The pair is visible once, here, and nowhere earlier.
41//!
42//! # What it does not do
43//!
44//! Only the pair that is next to itself. A reload with an instruction between it and the spill is
45//! left alone even when that instruction writes neither the register nor the slot, because
46//! answering that needs liveness over the written function rather than a look at the line above.
47//! A reload into a different register than the one spilled is left alone too, though a copy would
48//! do instead of a load. Both belong to the post-allocation copy propagation of section 37.4,
49//! which is a pass rather than a peephole, and this is deliberately the part of it that costs one
50//! walk over the function.
51
52use rucc_mir::{Block, Func, Inst};
53use rucc_regalloc::assign::Place;
54use rucc_regalloc::rewrite::Edit;
55use rucc_target::{PhysReg, RegClass};
56
57use crate::finish::Moves;
58
59/// Takes out every reload of a slot the instruction in front of it spilled, and says how many.
60pub fn dead(func: &mut Func, moves: &Moves) -> usize {
61    let blocks: Vec<Block> = func.blocks().collect();
62    let mut taken = 0;
63    for block in blocks {
64        let insts: Vec<Inst> = func.insts(block).collect();
65        // What the instruction last looked at wrote out, or `None` when it was not a spill. Only
66        // ever the instruction before, so a block starts with nothing and an instruction that is
67        // not one of the allocator's moves clears it.
68        let mut spilled: Option<(u32, PhysReg, RegClass)> = None;
69        let mut gone: Vec<Inst> = Vec::new();
70        for inst in insts {
71            let edit = moves.at(inst);
72            // The same slot, the same register and the same width as the spill in front of it,
73            // which makes this a read of a word that register still holds.
74            if spilled.is_some() && spilled == edit.and_then(reload) {
75                gone.push(inst);
76                // The spill stays what the next instruction is compared against, because taking
77                // this one out is what puts the next one next to it. A value read back twice
78                // running goes in one pass rather than one reload a pass.
79                continue;
80            }
81            spilled = edit.and_then(spill);
82        }
83        for inst in gone {
84            func.remove_inst(inst);
85            taken += 1;
86        }
87    }
88    taken
89}
90
91/// The slot a move writes a register out to, or `None` for a move that is not a spill.
92fn spill(edit: Edit) -> Option<(u32, PhysReg, RegClass)> {
93    match (edit.mov.to, edit.mov.from) {
94        (Place::Slot(slot), Place::Reg(reg)) => Some((slot, reg, edit.class)),
95        _ => None,
96    }
97}
98
99/// The slot a move reads a register back in from, or `None` for a move that is not a reload.
100fn reload(edit: Edit) -> Option<(u32, PhysReg, RegClass)> {
101    match (edit.mov.to, edit.mov.from) {
102        (Place::Reg(reg), Place::Slot(slot)) => Some((slot, reg, edit.class)),
103        _ => None,
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use rucc_base::Interner;
110    use rucc_mir::{Mem, Opcode, Operand, Reg};
111    use rucc_regalloc::moves::Move;
112    use rucc_regalloc::rewrite::At;
113    use rucc_target::x86_64::{FRAME, GPR, RAX, XMM};
114
115    use super::*;
116
117    /// A spill register to write with, which is the one x86-64 holds back for exactly this.
118    const R10: PhysReg = PhysReg::new(10);
119
120    /// A function with one block in it, and the names it was built with.
121    fn empty() -> (Interner, Func, Block) {
122        let mut names = Interner::new();
123        let mut func = Func::new(names.intern("f"));
124        let block = func.create_block();
125        (names, func, block)
126    }
127
128    /// The opcode of that name on this target.
129    fn op(names: &mut Interner, name: &str) -> Opcode {
130        Opcode::new(names.intern(&format!("{}{name}", FRAME.prefix)))
131    }
132
133    /// A store of a physical register to a frame address, as [`crate::finish`] writes a spill.
134    fn store(func: &mut Func, names: &mut Interner, block: Block, reg: PhysReg, at: i32) -> Inst {
135        let store = op(names, "mov_mr_64");
136        let base = Operand::read(Reg::physical(RAX), GPR);
137        func.build(block, store).uses(Reg::physical(reg), GPR).mem(Mem::at(base).plus(at)).finish()
138    }
139
140    /// A load of a physical register from a frame address, as it writes a reload.
141    fn load(func: &mut Func, names: &mut Interner, block: Block, reg: PhysReg, at: i32) -> Inst {
142        let load = op(names, "mov_rm_64");
143        let base = Operand::read(Reg::physical(RAX), GPR);
144        func.build(block, load).def(Reg::physical(reg), GPR).mem(Mem::at(base).plus(at)).finish()
145    }
146
147    /// What the allocator asked for, in the two shapes this pass is about.
148    ///
149    /// Where the edit was to go is not read by anything here, since what says two instructions are
150    /// next to each other is the function they were written into rather than what the allocator
151    /// said about where they belong.
152    fn out(block: Block, slot: u32, reg: PhysReg) -> Edit {
153        Edit {
154            at: At::StartOf(block),
155            mov: Move::new(Place::Slot(slot), Place::Reg(reg)),
156            class: GPR,
157        }
158    }
159
160    /// The other direction, and the one a dead reload is.
161    fn back(block: Block, slot: u32, reg: PhysReg) -> Edit {
162        Edit {
163            at: At::StartOf(block),
164            mov: Move::new(Place::Reg(reg), Place::Slot(slot)),
165            class: GPR,
166        }
167    }
168
169    /// How many instructions a block has left.
170    fn left(func: &Func, block: Block) -> usize {
171        func.insts(block).count()
172    }
173
174    /// The pair the whole pass is about: a word written out and read straight back into the
175    /// register it was written out of.
176    #[test]
177    fn a_reload_of_the_slot_the_instruction_in_front_of_it_spilled_goes() {
178        let (mut names, mut func, block) = empty();
179        let spill = store(&mut func, &mut names, block, R10, 16);
180        let reload = load(&mut func, &mut names, block, R10, 16);
181        let mut moves = Moves::default();
182        moves.record(spill, out(block, 0, R10));
183        moves.record(reload, back(block, 0, R10));
184
185        assert_eq!(dead(&mut func, &moves), 1);
186        assert_eq!(left(&func, block), 1, "the spill went too, or the reload stayed");
187        assert_eq!(func.insts(block).next(), Some(spill));
188    }
189
190    /// Two reads of one word, which is one spill and two reloads written next to each other, and
191    /// the second is as dead as the first because taking the first out is what puts it next to the
192    /// spill.
193    #[test]
194    fn a_run_of_reloads_of_one_slot_goes_in_a_single_pass() {
195        let (mut names, mut func, block) = empty();
196        let spill = store(&mut func, &mut names, block, R10, 16);
197        let first = load(&mut func, &mut names, block, R10, 16);
198        let second = load(&mut func, &mut names, block, R10, 16);
199        let mut moves = Moves::default();
200        moves.record(spill, out(block, 0, R10));
201        moves.record(first, back(block, 0, R10));
202        moves.record(second, back(block, 0, R10));
203
204        assert_eq!(dead(&mut func, &moves), 2);
205        assert_eq!(left(&func, block), 1);
206    }
207
208    /// The instruction between them is what the value was spilled for, and it may write the
209    /// register, so the reload behind it is a read of a word that register no longer holds.
210    #[test]
211    fn a_reload_with_an_instruction_between_it_and_the_spill_stays() {
212        let (mut names, mut func, block) = empty();
213        let spill = store(&mut func, &mut names, block, R10, 16);
214        let between = op(&mut names, "add_rr_64");
215        func.build(block, between)
216            .def(Reg::physical(R10), GPR)
217            .uses(Reg::physical(RAX), GPR)
218            .finish();
219        let reload = load(&mut func, &mut names, block, R10, 16);
220        let mut moves = Moves::default();
221        moves.record(spill, out(block, 0, R10));
222        moves.record(reload, back(block, 0, R10));
223
224        assert_eq!(dead(&mut func, &moves), 0);
225        assert_eq!(left(&func, block), 3);
226    }
227
228    /// A reload of another slot reads another word, whatever address the two instructions were
229    /// written with.
230    #[test]
231    fn a_reload_of_a_different_slot_stays() {
232        let (mut names, mut func, block) = empty();
233        let spill = store(&mut func, &mut names, block, R10, 16);
234        let reload = load(&mut func, &mut names, block, R10, 24);
235        let mut moves = Moves::default();
236        moves.record(spill, out(block, 0, R10));
237        moves.record(reload, back(block, 1, R10));
238
239        assert_eq!(dead(&mut func, &moves), 0);
240        assert_eq!(left(&func, block), 2);
241    }
242
243    /// A reload into another register puts the word somewhere it is not, so the load does
244    /// something even though the word it reads is the one just written.
245    #[test]
246    fn a_reload_into_a_different_register_stays() {
247        let (mut names, mut func, block) = empty();
248        let spill = store(&mut func, &mut names, block, R10, 16);
249        let reload = load(&mut func, &mut names, block, PhysReg::new(11), 16);
250        let mut moves = Moves::default();
251        moves.record(spill, out(block, 0, R10));
252        moves.record(reload, back(block, 0, PhysReg::new(11)));
253
254        assert_eq!(dead(&mut func, &moves), 0);
255        assert_eq!(left(&func, block), 2);
256    }
257
258    /// A slot number is a slot number in the class that owns it, so a pair that agrees on
259    /// everything but the class is two different words and two registers that share a number.
260    #[test]
261    fn a_reload_of_another_class_stays() {
262        let (mut names, mut func, block) = empty();
263        let spill = store(&mut func, &mut names, block, R10, 16);
264        let reload = load(&mut func, &mut names, block, R10, 16);
265        let mut moves = Moves::default();
266        moves.record(spill, out(block, 0, R10));
267        moves.record(reload, Edit { class: XMM, ..back(block, 0, R10) });
268
269        assert_eq!(dead(&mut func, &moves), 0);
270        assert_eq!(left(&func, block), 2);
271    }
272
273    /// The same two instructions, with nothing saying the allocator wrote them, which is what a
274    /// write to a local variable and a read of it back look like.
275    #[test]
276    fn a_store_and_a_load_the_allocator_did_not_write_stay() {
277        let (mut names, mut func, block) = empty();
278        store(&mut func, &mut names, block, R10, 16);
279        load(&mut func, &mut names, block, R10, 16);
280
281        assert_eq!(dead(&mut func, &Moves::default()), 0);
282        assert_eq!(left(&func, block), 2);
283    }
284
285    /// The last instruction of one block and the first of another are not next to each other. What
286    /// ran before the second block is whatever jumped to it, which is any block that names it and
287    /// not the one the text happens to be under.
288    #[test]
289    fn a_reload_at_the_top_of_another_block_stays() {
290        let (mut names, mut func, first) = empty();
291        let second = func.create_block();
292        let spill = store(&mut func, &mut names, first, R10, 16);
293        let reload = load(&mut func, &mut names, second, R10, 16);
294        let mut moves = Moves::default();
295        moves.record(spill, out(first, 0, R10));
296        moves.record(reload, back(first, 0, R10));
297
298        assert_eq!(dead(&mut func, &moves), 0);
299        assert_eq!(left(&func, second), 1);
300    }
301
302    /// A copy is a move of the allocator's like any other and is not a spill, so the reload behind
303    /// one is compared against nothing.
304    #[test]
305    fn a_copy_between_the_two_is_not_a_spill_to_read_back() {
306        let (mut names, mut func, block) = empty();
307        let spill = store(&mut func, &mut names, block, R10, 16);
308        let copy = op(&mut names, "mov_rr_64");
309        let copied = func
310            .build(block, copy)
311            .def(Reg::physical(RAX), GPR)
312            .uses(Reg::physical(R10), GPR)
313            .finish();
314        let reload = load(&mut func, &mut names, block, R10, 16);
315        let mut moves = Moves::default();
316        moves.record(spill, out(block, 0, R10));
317        moves.record(
318            copied,
319            Edit {
320                at: At::StartOf(block),
321                mov: Move::new(Place::Reg(RAX), Place::Reg(R10)),
322                class: GPR,
323            },
324        );
325        moves.record(reload, back(block, 0, R10));
326
327        assert_eq!(dead(&mut func, &moves), 0);
328        assert_eq!(left(&func, block), 3);
329    }
330}