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::live::Live;
162 use rucc_regalloc::moves::Move;
163 use rucc_regalloc::order::Order;
164 use rucc_regalloc::rewrite::{At, Edit};
165 use rucc_target::x86_64::{GPR, SYSV};
166
167 use super::*;
168
169 /// An allocation holding the moves given and nothing else, which is all the counting reads.
170 fn allocated(moves: &[(Place, Place)]) -> Allocation {
171 let mut names = Interner::new();
172 let mut func = Func::new(names.intern("f"));
173 let block = func.create_block();
174 let edits = moves
175 .iter()
176 .map(|&(to, from)| Edit {
177 at: At::StartOf(block),
178 mov: Move::new(to, from),
179 class: GPR,
180 })
181 .collect();
182 let order = Order::of(&func);
183 let live = Live::of(&func, &order);
184 Allocation { assignment: Assignment::empty(0), edits, order, live }
185 }
186
187 #[test]
188 fn a_write_into_a_slot_is_a_store_and_a_read_out_of_one_is_a_reload() {
189 // The two are counted apart because they are different costs: a value spilled once and
190 // read in a loop stores once and reloads every time round.
191 let reg = Place::Reg(SYSV.int_order[0]);
192 let cost = Cost::of(&allocated(&[
193 (Place::Slot(0), reg),
194 (reg, Place::Slot(0)),
195 (reg, Place::Slot(0)),
196 ]));
197 assert_eq!(cost.stores, 1);
198 assert_eq!(cost.reloads, 2);
199 }
200
201 #[test]
202 fn a_move_from_one_slot_to_another_is_both() {
203 // It goes through a scratch register, because no machine here has memory to memory, so
204 // the program really does pay for a load and a store.
205 let cost = Cost::of(&allocated(&[(Place::Slot(1), Place::Slot(0))]));
206 assert_eq!(cost.stores, 1);
207 assert_eq!(cost.reloads, 1);
208 }
209
210 #[test]
211 fn the_listing_holds_every_function_and_the_totals_are_the_sum_of_them() {
212 let mut pressure = Pressure::new();
213 pressure.record("f", Cost { slots: 2, stores: 3, reloads: 4 });
214 pressure.record("g", Cost::default());
215 let mut second = Pressure::new();
216 second.record("h", Cost { slots: 1, stores: 1, reloads: 5 });
217 pressure.merge(&second);
218
219 assert_eq!(pressure.functions(), 3);
220 assert_eq!(pressure.total(), Cost { slots: 3, stores: 4, reloads: 9 });
221
222 let listing = pressure.listing();
223 let lines: Vec<&str> = listing.lines().collect();
224 assert_eq!(lines.len(), 4, "{listing}");
225 assert!(lines[0].contains("3 functions, 3 slots, 4 stores, 9 reloads"), "{}", lines[0]);
226 assert_eq!(lines[1], "2 3 4 f");
227 // The function that spilled nothing is listed too, so the file says how much was measured.
228 assert_eq!(lines[2], "0 0 0 g");
229 assert_eq!(lines[3], "1 1 5 h");
230 }
231
232 #[test]
233 fn a_function_that_runs_out_of_registers_is_recorded_as_having_spilled() {
234 // End to end through the allocator rather than through a made up edit list, so that the
235 // three counts are the ones a real allocation produces.
236 let mut names = Interner::new();
237 let mut func = Func::new(names.intern("f"));
238 let opcode = Opcode::new(names.intern("x64.nop"));
239 let block = func.create_block();
240 let first = func.new_vreg(GPR);
241 let second = func.new_vreg(GPR);
242 let third = func.new_vreg(GPR);
243 func.build(block, opcode).def(first, GPR).finish();
244 func.build(block, opcode).def(second, GPR).finish();
245 func.build(block, opcode).def(third, GPR).finish();
246 func.build(block, opcode).uses(first, GPR).uses(second, GPR).uses(third, GPR).finish();
247
248 // Two registers to hand out and three values all wanted at once, so one goes to the stack.
249 let env = Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
250 let cost = Cost::of(&rucc_regalloc::run(&mut func, &env, "f"));
251 assert_eq!(cost.slots, 1);
252 assert_eq!(cost.stores, 1);
253 assert_eq!(cost.reloads, 1);
254 }
255}