rucc_codegen/pressure.rs
1//! How much of the frame the register allocator had to use, function by function.
2//!
3//! Design: `spec/safe-memory/13-performance.md` section 13.1, whose table of metrics has a row for
4//! spill and fill counts, and section 13.2.1, which says why: a capability in flight is four words
5//! in registers, and if materializing one pushes something else onto the stack in a hot loop then
6//! no amount of check elimination saves us and the representation is what has to change. Milestone
7//! S4 in `spec/safe-memory/16-milestones.md` asks for the delta on the pointer heavy benchmarks,
8//! and `cargo xtask pressure` is what reads these numbers back.
9//!
10//! # What is counted
11//!
12//! Three numbers per function. The slots are how many values the allocator could not keep in a
13//! register at all, which is what the frame grows by. The stores are how many times one of them is
14//! written to its slot and the reloads are how many times one is read back, which is what the
15//! program pays at run time and is not the same number: a value spilled once and read in a loop
16//! costs one store and as many reloads as the loop has instructions that want it.
17//!
18//! A move from one slot to another counts as both, because it is both. That happens on an edge
19//! carrying a spilled value into a parameter that was itself spilled, and no machine here has an
20//! instruction for it, so it goes through a scratch register and really is a load and a store.
21//!
22//! # What the numbers are not
23//!
24//! Not a claim about the best allocator we could have. There is one allocator in this compiler and
25//! it is the single pass one `spec/10-backend.md` section 10.4 describes, so a function that spills
26//! here might not spill under the backtracking allocator M4 brings. What the delta between two
27//! builds of the same program says is how much more pressure the instrumented one puts on whatever
28//! allocator is reading it, and that comparison is fair as long as both sides go through the same
29//! one.
30
31use std::fmt::Write as _;
32
33use rucc_regalloc::Allocation;
34use rucc_regalloc::assign::Place;
35
36/// What allocating one function cost.
37#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
38pub struct Cost {
39 /// Values that went to the stack, which is what the frame grows by.
40 pub slots: usize,
41 /// Writes into a slot.
42 pub stores: usize,
43 /// Reads out of a slot.
44 pub reloads: usize,
45}
46
47impl Cost {
48 /// What one allocation came to.
49 #[must_use]
50 pub fn of(allocation: &Allocation) -> Self {
51 let mut cost = Self { slots: allocation.assignment.spilled(), ..Self::default() };
52 for edit in &allocation.edits {
53 if matches!(edit.mov.to, Place::Slot(_)) {
54 cost.stores += 1;
55 }
56 if matches!(edit.mov.from, Place::Slot(_)) {
57 cost.reloads += 1;
58 }
59 }
60 cost
61 }
62
63 /// Takes in another one, which is how a whole module or a whole command line is added up.
64 fn add(&mut self, other: Self) {
65 self.slots += other.slots;
66 self.stores += other.stores;
67 self.reloads += other.reloads;
68 }
69}
70
71/// One function, and what it cost.
72#[derive(Debug, Clone, PartialEq, Eq)]
73struct Row {
74 /// What the function is called, which is the symbol name and not the name in the source.
75 name: String,
76 /// What allocating it came to.
77 cost: Cost,
78}
79
80/// What every function a run allocated cost, in the order they were allocated.
81///
82/// Kept per function rather than as one total, because the number that matters is a hot loop and
83/// the way to find one in a file the size of an amalgamation is to sort the rows. The total is
84/// there too, since it is what a comparison of two builds is usually about and nobody should have
85/// to add up ten thousand lines to get it.
86#[derive(Debug, Default, Clone, PartialEq, Eq)]
87pub struct Pressure {
88 /// One per function, in the order they came through.
89 rows: Vec<Row>,
90}
91
92impl Pressure {
93 /// Nothing recorded yet.
94 #[must_use]
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 /// Writes down what one function cost.
100 pub fn record(&mut self, name: &str, cost: Cost) {
101 self.rows.push(Row { name: name.to_owned(), cost });
102 }
103
104 /// Takes in everything another one recorded, which is how one file's answer joins a run's.
105 pub fn merge(&mut self, other: &Self) {
106 self.rows.extend(other.rows.iter().cloned());
107 }
108
109 /// How many functions were allocated.
110 #[must_use]
111 pub fn functions(&self) -> usize {
112 self.rows.len()
113 }
114
115 /// Every function's cost added together.
116 #[must_use]
117 pub fn total(&self) -> Cost {
118 let mut total = Cost::default();
119 for row in &self.rows {
120 total.add(row.cost);
121 }
122 total
123 }
124
125 /// What `-Zregister-pressure=FILE` writes.
126 ///
127 /// A comment holding the totals and then one line per function, each of them the three counts
128 /// and then the name. The counts come first because they are the fields a reader is sorting
129 /// on and the name is the one field that could be any length, which is the layout
130 /// `-Zrule-coverage` uses for the same reason.
131 ///
132 /// Every function is listed, including the ones that spilled nothing, so that one of these
133 /// files says how much of the module was measured as well as what the answer was. A build that
134 /// stopped early and a build that spilled nowhere would otherwise look the same.
135 #[must_use]
136 pub fn listing(&self) -> String {
137 let total = self.total();
138 let mut out = format!(
139 "# rucc register pressure: {} functions, {} slots, {} stores, {} reloads\n",
140 self.rows.len(),
141 total.slots,
142 total.stores,
143 total.reloads
144 );
145 for row in &self.rows {
146 let _ = writeln!(
147 out,
148 "{} {} {} {}",
149 row.cost.slots, row.cost.stores, row.cost.reloads, row.name
150 );
151 }
152 out
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use rucc_base::Interner;
159 use rucc_mir::{Func, Opcode};
160 use rucc_regalloc::assign::{Assignment, Env};
161 use rucc_regalloc::moves::Move;
162 use rucc_regalloc::rewrite::{At, Edit};
163 use rucc_target::x86_64::{GPR, SYSV};
164
165 use super::*;
166
167 /// An allocation holding the moves given and nothing else, which is all the counting reads.
168 fn allocated(moves: &[(Place, Place)]) -> Allocation {
169 let mut names = Interner::new();
170 let mut func = Func::new(names.intern("f"));
171 let block = func.create_block();
172 let edits = moves
173 .iter()
174 .map(|&(to, from)| Edit {
175 at: At::StartOf(block),
176 mov: Move::new(to, from),
177 class: GPR,
178 })
179 .collect();
180 Allocation { assignment: Assignment::empty(0), edits }
181 }
182
183 #[test]
184 fn a_write_into_a_slot_is_a_store_and_a_read_out_of_one_is_a_reload() {
185 // The two are counted apart because they are different costs: a value spilled once and
186 // read in a loop stores once and reloads every time round.
187 let reg = Place::Reg(SYSV.int_order[0]);
188 let cost = Cost::of(&allocated(&[
189 (Place::Slot(0), reg),
190 (reg, Place::Slot(0)),
191 (reg, Place::Slot(0)),
192 ]));
193 assert_eq!(cost.stores, 1);
194 assert_eq!(cost.reloads, 2);
195 }
196
197 #[test]
198 fn a_move_from_one_slot_to_another_is_both() {
199 // It goes through a scratch register, because no machine here has memory to memory, so
200 // the program really does pay for a load and a store.
201 let cost = Cost::of(&allocated(&[(Place::Slot(1), Place::Slot(0))]));
202 assert_eq!(cost.stores, 1);
203 assert_eq!(cost.reloads, 1);
204 }
205
206 #[test]
207 fn the_listing_holds_every_function_and_the_totals_are_the_sum_of_them() {
208 let mut pressure = Pressure::new();
209 pressure.record("f", Cost { slots: 2, stores: 3, reloads: 4 });
210 pressure.record("g", Cost::default());
211 let mut second = Pressure::new();
212 second.record("h", Cost { slots: 1, stores: 1, reloads: 5 });
213 pressure.merge(&second);
214
215 assert_eq!(pressure.functions(), 3);
216 assert_eq!(pressure.total(), Cost { slots: 3, stores: 4, reloads: 9 });
217
218 let listing = pressure.listing();
219 let lines: Vec<&str> = listing.lines().collect();
220 assert_eq!(lines.len(), 4, "{listing}");
221 assert!(lines[0].contains("3 functions, 3 slots, 4 stores, 9 reloads"), "{}", lines[0]);
222 assert_eq!(lines[1], "2 3 4 f");
223 // The function that spilled nothing is listed too, so the file says how much was measured.
224 assert_eq!(lines[2], "0 0 0 g");
225 assert_eq!(lines[3], "1 1 5 h");
226 }
227
228 #[test]
229 fn a_function_that_runs_out_of_registers_is_recorded_as_having_spilled() {
230 // End to end through the allocator rather than through a made up edit list, so that the
231 // three counts are the ones a real allocation produces.
232 let mut names = Interner::new();
233 let mut func = Func::new(names.intern("f"));
234 let opcode = Opcode::new(names.intern("x64.nop"));
235 let block = func.create_block();
236 let first = func.new_vreg(GPR);
237 let second = func.new_vreg(GPR);
238 let third = func.new_vreg(GPR);
239 func.build(block, opcode).def(first, GPR).finish();
240 func.build(block, opcode).def(second, GPR).finish();
241 func.build(block, opcode).def(third, GPR).finish();
242 func.build(block, opcode).uses(first, GPR).uses(second, GPR).uses(third, GPR).finish();
243
244 // Two registers to hand out and three values all wanted at once, so one goes to the stack.
245 let env = Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
246 let cost = Cost::of(&rucc_regalloc::run(&mut func, &env, "f"));
247 assert_eq!(cost.slots, 1);
248 assert_eq!(cost.stores, 1);
249 assert_eq!(cost.reloads, 1);
250 }
251}