Skip to main content

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}