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}