Skip to main content

rucc_cost/
lib.rs

1//! What things cost on a target, and every tuning constant the optimizer has.
2//!
3//! This is document 40 of `spec/optimizer/`, made real. The crate exists because of the failure
4//! that document opens with: a compiler where each pass has its own idea of what an operation
5//! costs is a compiler where two passes undo each other, and neither of them is wrong. Putting the
6//! numbers in one place does not make them right, but it makes them one thing that can be measured
7//! and changed, instead of thirty things that have to be found first.
8//!
9//! # What is in here
10//!
11//! [`Cycles`] and [`Bytes`], which are separate types so that a cost in time is never compared
12//! against a threshold in space. [`Cost`], which is a time and a complexity compared
13//! lexicographically with an explicit infinity, from GCC's `comp_cost`. [`CostTable`], which a
14//! target fills in completely or not at all. [`TuneFlag`], which is the half of a cost model that
15//! is a boolean rather than a number. And [`heuristics`], which is the file every threshold in
16//! every pass has to come from.
17//!
18//! # Two tables, not one table and a policy
19//!
20//! Section 40.3 reads `ix86_cur_cost()` at `gcc/config/i386/i386.h:269` and takes the design
21//! from it: optimizing for size is a different cost table, not a weighting applied to the same
22//! one. `-Os` selects the second table and every pass then goes on asking the same questions in
23//! the same way. It makes `-Os` behaviour inspectable as data, and it means no pass has to
24//! remember to ask whether it is optimizing for size, which is the sort of thing a pass forgets
25//! in exactly one of its five decisions.
26//!
27//! # What is not in here
28//!
29//! Anything derived from a function. Register pressure, block frequency and branch predictability
30//! are all things section 40.6 wants computed once per function and shared, and all three need the
31//! IR, so they belong with the analyses rather than with the target description. This crate is
32//! below the IR on purpose.
33
34#![doc(html_root_url = "https://docs.rs/rucc-cost/0.10.2")]
35
36pub mod cost;
37pub mod cycles;
38pub mod heuristics;
39pub mod table;
40pub mod tune;
41pub mod x86_64;
42
43pub use cost::{Complexity, Cost};
44pub use cycles::{Bytes, Cycles};
45pub use table::{AddrMode, Builder, CostTable, Width};
46pub use tune::{TuneFlag, Tuning};
47
48use rucc_target::Arch;
49use rucc_tuple::TargetTuple;
50
51/// Which of a target's two tables is wanted.
52///
53/// A named type rather than a bare `bool`, because `table(true)` at a call site is a coin toss for
54/// the reader and `table(Goal::Size)` is not.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub enum Goal {
57    /// Make it fast. `-O1`, `-O2`, `-O3`.
58    Speed,
59    /// Make it small. `-Os` and `-Oz`.
60    Size,
61}
62
63impl Goal {
64    /// The goal for a level that has already been asked whether it optimizes for size.
65    ///
66    /// Takes the answer rather than the level, because the level lives in `rucc-session` and this
67    /// crate sits below it. That is not a workaround for the layer rule, it is the layer rule
68    /// working: what an instruction costs on a machine has nothing to do with how the driver was
69    /// invoked, and a dependency the other way would say it did. The caller writes
70    /// `Goal::for_size(level.is_size())`, which is one line and reads correctly.
71    #[must_use]
72    pub const fn for_size(size: bool) -> Self {
73        if size { Self::Size } else { Self::Speed }
74    }
75
76    /// The goal as it appears in a dump.
77    #[must_use]
78    pub const fn as_str(self) -> &'static str {
79        match self {
80            Self::Speed => "speed",
81            Self::Size => "size",
82        }
83    }
84}
85
86impl std::fmt::Display for Goal {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.write_str(self.as_str())
89    }
90}
91
92/// Which bank of registers a value needs.
93///
94/// Two, which is the split every target rucc has a back end for or plans one for. A vector lands
95/// in the floating point bank because on x86-64 the same registers hold both, and a target where
96/// that is wrong is a target that needs a third variant here rather than a different rule.
97///
98/// It is in this crate rather than with the pressure analysis that counts them because the target
99/// is what says how many of each it hands out, and this crate is where a target is described. The
100/// question of which bank holds a given IR type is the other half and it stays with the analysis,
101/// since this crate sits below the IR.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub enum RegClass {
104    /// Integers, pointers and capabilities, which the general purpose registers hold.
105    Integer,
106    /// Floating point and vectors, which on every target rucc targets share a bank.
107    Float,
108}
109
110impl RegClass {
111    /// Both of them, for a caller that reports each.
112    pub const ALL: [Self; 2] = [Self::Integer, Self::Float];
113
114    /// How many there are, for the arrays keyed by one.
115    pub const COUNT: usize = Self::ALL.len();
116
117    /// Where this bank sits in a class-indexed array.
118    #[must_use]
119    pub const fn index(self) -> usize {
120        self as usize
121    }
122
123    /// How it reads in a dump.
124    #[must_use]
125    pub const fn as_str(self) -> &'static str {
126        match self {
127            Self::Integer => "integer",
128            Self::Float => "float",
129        }
130    }
131}
132
133impl std::fmt::Display for RegClass {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.write_str(self.as_str())
136    }
137}
138
139/// What a pass asks a target about costs, per section 40.12.
140///
141/// Three kinds of answer: a number that comes from one of the two tables, a boolean that does not
142/// depend on the goal at all, and a number that does not either. Whether a microarchitecture
143/// prefers an `lea` to an `add` is not a different fact when optimizing for size, and neither is
144/// how many registers the allocator hands out.
145pub trait TargetCosts: Send + Sync {
146    /// The table for this goal.
147    fn table(&self, goal: Goal) -> &CostTable;
148
149    /// What this target answers for a tuning flag.
150    fn tune(&self, flag: TuneFlag) -> bool;
151
152    /// How many registers of that bank the allocator will actually hand out, per section 40.6.
153    ///
154    /// Not how many the machine has. The number a pass wants is how many a value may be put in
155    /// after the ones with a job of their own are taken out, because a pass that hoists up to the
156    /// architectural count has hoisted into registers that were never available and the spill it
157    /// caused lands inside the loop it was trying to help.
158    ///
159    /// There is no default. A target that has not answered this has not been asked how its
160    /// register file is spent, and a default here would be a number nobody chose that every pass
161    /// consulting it would believe.
162    fn allocatable(&self, class: RegClass) -> u32;
163
164    /// What the target is called in a dump.
165    fn name(&self) -> &'static str;
166
167    /// What an unpredictable branch costs at this goal, per section 40.5.
168    ///
169    /// Provided rather than left to each pass, because `BRANCH_COST` at
170    /// `gcc/config/i386/i386.h:2023` is three cases in one line and getting one of them wrong is
171    /// how a well predicted branch ends up if-converted.
172    ///
173    /// Two of the three cases read the table. Optimizing for speed, a predictable branch is free
174    /// and an unpredictable one costs whatever the target says; optimizing for size, a branch is
175    /// the same number of bytes either way, because the branch predictor does not shorten the
176    /// encoding. The one case that does not read the table is the free one, and it does not
177    /// because zero is a claim about hardware rather than about this machine.
178    fn branch_cost(&self, goal: Goal, predictable: bool) -> Cycles {
179        if goal == Goal::Speed && predictable {
180            return heuristics::BRANCH_COST_PREDICTABLE;
181        }
182        self.table(goal).branch_cost
183    }
184}
185
186/// The costs for a target, or nothing for one nobody has written a table for.
187///
188/// x86-64 is the only answer today, because it is the only back end rucc has. The function exists
189/// anyway so that the second target is a file and a match arm rather than a redesign.
190#[must_use]
191pub fn for_arch(arch: Arch) -> Option<&'static dyn TargetCosts> {
192    match arch {
193        Arch::X86_64 => Some(x86_64::COSTS),
194        _ => None,
195    }
196}
197
198/// The costs for the target a tuple names, which is what a caller holding a module asks.
199///
200/// Two enumerations name the same three machines. `rucc_tuple::Arch` is what a target tuple
201/// carries and it can spell architectures rucc has no back end for, and [`Arch`] is what the cost
202/// tables are keyed by because it is the list of architectures a back end exists for. So the
203/// mapping is partial, and everything it does not name answers the same as an architecture with a
204/// name and no table.
205///
206/// It is here rather than at the caller because there is one right mapping and a second copy of it
207/// somewhere else would be a place for the two to disagree.
208#[must_use]
209pub fn for_tuple(tuple: TargetTuple) -> Option<&'static dyn TargetCosts> {
210    let arch = match tuple.arch() {
211        rucc_tuple::Arch::X86_64 => Arch::X86_64,
212        rucc_tuple::Arch::Aarch64 => Arch::Aarch64,
213        rucc_tuple::Arch::Riscv64 => Arch::Riscv64,
214        _ => return None,
215    };
216    for_arch(arch)
217}
218
219#[cfg(test)]
220mod tests {
221    use super::{Goal, TuneFlag, for_arch};
222    use rucc_target::Arch;
223
224    #[test]
225    fn the_only_backend_has_costs() {
226        let costs = for_arch(Arch::X86_64).expect("x86-64 is the back end rucc has");
227        assert_eq!(costs.name(), "x86-64");
228        assert!(!costs.table(Goal::Speed).add.is_infinite());
229    }
230
231    #[test]
232    fn a_target_with_no_back_end_has_no_costs_rather_than_made_up_ones() {
233        // The alternative would be a default table, and a default table is a set of numbers
234        // nobody chose that every pass would believe.
235        assert!(for_arch(Arch::Aarch64).is_none());
236    }
237
238    #[test]
239    fn a_tuning_flag_does_not_depend_on_the_goal() {
240        // There is nothing to assert against here except the shape of the interface: `tune` takes
241        // no goal, so it cannot answer differently for `-Os`. The test is here to fail if
242        // somebody adds one.
243        let costs = for_arch(Arch::X86_64).unwrap();
244        for flag in TuneFlag::ALL {
245            let _: bool = costs.tune(flag);
246        }
247    }
248}