rucc_opt/machine.rs
1//! What a pass knows about the machine it is compiling for.
2//!
3//! Design: section 40.12 of `spec/optimizer/40-cost-models.md`, which writes down the interface a
4//! pass asks a target through, and tamnd/rucc#655, which is the observation that no pass could
5//! reach it. The cost tables have existed since the crate did and nothing outside `rucc-cost` ever
6//! called `for_arch`, because a pass is handed a function and a function does not know what it is
7//! being compiled for.
8//!
9//! # Why it sits on the analysis cache
10//!
11//! [`crate::Analyses`] is the one thing every pass is already handed besides the function and the
12//! fuel, so putting the machine there is what makes it reachable without a fourth argument on
13//! every `run`. The cache is per function and so is the machine's lifetime as far as a pass is
14//! concerned, and the pipeline builds one for the module and hands out copies.
15//!
16//! It is a copy rather than a borrow because it is two words, a pointer to a table nobody writes
17//! and the goal. A pass that wants both the machine and an analysis out of the same cache would
18//! otherwise be holding two borrows of it, one of them mutable, which is a fight with the borrow
19//! checker over a value cheaper to copy than to reference.
20//!
21//! # A target with no back end
22//!
23//! `rucc_cost::for_arch` answers `None` for AArch64 and RISC-V, because neither has a back end and
24//! neither has a cost table, and a default table would be numbers nobody chose that every pass
25//! would believe. So [`Machine::costs`] is an `Option` and a pass that needs a number has to say
26//! what it does without one. What it should do is nothing, and say so as a missed remark: an
27//! optimization that guessed at the cost model would be a pass tuned for a machine that is not the
28//! one being compiled for.
29
30use rucc_cost::{CostTable, Cycles, Goal, RegClass, TargetCosts, TuneFlag};
31use rucc_ir::Module;
32use rucc_session::OptLevel;
33
34/// The target a pass is compiling for, and which of its two cost tables applies.
35#[derive(Clone, Copy)]
36pub struct Machine {
37 costs: Option<&'static dyn TargetCosts>,
38 goal: Goal,
39}
40
41impl Machine {
42 /// The machine a module is being compiled for at that level.
43 ///
44 /// Both halves come from things the pipeline already has, which is why no flag was added for
45 /// this. The architecture is on the module, because a module is built for a target and says
46 /// so, and the goal is whether the level optimizes for size, which is one call on the level.
47 #[must_use]
48 pub fn of(module: &Module, level: OptLevel) -> Self {
49 Self::with(rucc_cost::for_tuple(module.tuple), Goal::for_size(level.is_size()))
50 }
51
52 /// The machine for costs already in hand.
53 ///
54 /// What [`Machine::of`] is written in terms of, and what a caller that resolved a target some
55 /// other way uses. `None` is a target with no cost table, which is every architecture rucc has
56 /// no back end for.
57 #[must_use]
58 pub const fn with(costs: Option<&'static dyn TargetCosts>, goal: Goal) -> Self {
59 Self { costs, goal }
60 }
61
62 /// A machine nobody has a cost table for, which is what a test that does not care wants.
63 ///
64 /// Named for what it is rather than called `default`, because a default machine is the thing
65 /// this module exists to avoid: a pass that got one silently would be optimizing for a target
66 /// that does not exist.
67 #[must_use]
68 pub const fn unknown() -> Self {
69 Self { costs: None, goal: Goal::Speed }
70 }
71
72 /// The costs for this target, or nothing for one with no table.
73 #[must_use]
74 pub fn costs(self) -> Option<&'static dyn TargetCosts> {
75 self.costs
76 }
77
78 /// Which table applies, which is the goal the level asked for.
79 #[must_use]
80 pub const fn goal(self) -> Goal {
81 self.goal
82 }
83
84 /// The cost table for this target and goal, or nothing for a target with no table.
85 #[must_use]
86 pub fn table(self) -> Option<&'static CostTable> {
87 self.costs.map(|costs| costs.table(self.goal))
88 }
89
90 /// What this target answers for a tuning flag, and the flag's documented default without one.
91 ///
92 /// A default is right here and wrong for a number, which is the asymmetry `Tuning::untuned`
93 /// already records: a tuning flag is a question whose safe answer is written down, and a cost
94 /// is a measurement of a machine.
95 #[must_use]
96 pub fn tune(self, flag: TuneFlag) -> bool {
97 self.costs.is_some_and(|costs| costs.tune(flag))
98 }
99
100 /// How many registers of that bank the allocator hands out, or nothing without a target.
101 #[must_use]
102 pub fn allocatable(self, class: RegClass) -> Option<u32> {
103 self.costs.map(|costs| costs.allocatable(class))
104 }
105
106 /// What a branch of that predictability costs, or nothing without a target.
107 #[must_use]
108 pub fn branch_cost(self, predictable: bool) -> Option<Cycles> {
109 self.costs.map(|costs| costs.branch_cost(self.goal, predictable))
110 }
111
112 /// What the machine is called in a dump, and `unknown` for a target with no table.
113 #[must_use]
114 pub fn name(self) -> &'static str {
115 self.costs.map_or("unknown", TargetCosts::name)
116 }
117}
118
119impl std::fmt::Debug for Machine {
120 /// The name and the goal, because a `dyn TargetCosts` has no `Debug` and the pointer would say
121 /// nothing to anybody reading a dump.
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "Machine({}, {})", self.name(), self.goal)
124 }
125}
126
127/// Test helpers, which are here rather than in `crate::testing` because that module is also read
128/// by two integration tests through a `#[path]` include and so cannot name anything in the crate.
129#[cfg(test)]
130pub(crate) mod fixtures {
131 use super::Machine;
132 use crate::analysis::Analyses;
133 use rucc_cost::Goal;
134
135 /// An empty analysis cache for the target the tests are written against.
136 ///
137 /// x86-64, because it is the only one with a cost table and a test that reads a cost wants a
138 /// real number rather than a `None` that makes the pass under test do nothing. A test that
139 /// means to check what a pass does without a target says so with `Machine::unknown` at the
140 /// call site.
141 pub(crate) fn analyses() -> Analyses {
142 Analyses::new(machine())
143 }
144
145 /// The machine the tests are written against, which is x86-64 optimizing for speed.
146 pub(crate) fn machine() -> Machine {
147 Machine::with(rucc_cost::for_arch(rucc_target::Arch::X86_64), Goal::Speed)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::Machine;
154 use rucc_cost::{Goal, RegClass};
155
156 #[test]
157 fn a_machine_nobody_has_a_table_for_answers_nothing_rather_than_a_number() {
158 let machine = Machine::unknown();
159 assert!(machine.costs().is_none());
160 assert!(machine.table().is_none());
161 assert!(machine.allocatable(RegClass::Integer).is_none());
162 assert!(machine.branch_cost(false).is_none());
163 assert_eq!(machine.name(), "unknown");
164 }
165
166 #[test]
167 fn the_two_banks_of_x86_64_are_not_the_same_size() {
168 // The general purpose bank gives up the stack pointer and the frame pointer and the vector
169 // bank gives up neither, so a loop holding only floating point values has two more
170 // registers of room than one holding integers. The pass that asked before this existed
171 // read one constant for both and got the smaller.
172 let machine = Machine::with(rucc_cost::for_arch(rucc_target::Arch::X86_64), Goal::Speed);
173 assert_eq!(machine.allocatable(RegClass::Integer), Some(12));
174 assert_eq!(machine.allocatable(RegClass::Float), Some(14));
175 }
176}