rucc_opt/stats.rs
1//! What a pass has to say about what it did, and about what it wanted to do and could not.
2//!
3//! Section 42.2 of `spec/optimizer/42-measurement.md` counted the instrumented events in GCC and
4//! got about a hundred across a compiler with three hundred passes, concentrated in the dozen
5//! files somebody had already spent a bad week in. The shape of that number is the problem: a
6//! counter you call is a counter you can forget to call, so instrumentation ends up where
7//! somebody was already suffering rather than everywhere it is needed.
8//!
9//! So here a pass does not call a counter. It returns one. [`crate::Pass::run`] hands back a
10//! [`Stats`] and there is no other way for a pass to say it changed anything, because
11//! [`Stats::changed`] is what the pass manager reads to decide whether to run the verifier and
12//! whether the dumps are worth taking. A pass that transforms without recording is a pass whose
13//! transformation the manager does not believe happened, which fails the tests in
14//! [`crate::pipeline`] rather than quietly working. That is the one structural improvement over
15//! GCC that section 42.2 asks for, and it is only available before there are passes to retrofit.
16//!
17//! # The three kinds
18//!
19//! `optimized`, `missed` and `note`, which are three of the four keywords GCC's `-fopt-info`
20//! takes, and they mean the same things here. The one that matters is `missed`. A pass that only
21//! reports its successes cannot be tuned, because the question a person has at a slow loop is not
22//! what the compiler did, it is what the compiler nearly did. Every pass in here is expected to
23//! have at least one `missed` site, and a pass that has none is a pass that has not been asked
24//! the question yet.
25//!
26//! # Why the text is `&'static str`
27//!
28//! An event names a site in a pass rather than a fact about a program, so the set of them is
29//! fixed at compile time and small. That is what makes the counts addable: two runs over
30//! different files produce counts of the same named things, so the corpus can total them across a
31//! thousand programs and get a number that means something. A formatted string carrying a
32//! variable in it would make every event unique and every total one.
33
34use std::fmt;
35
36/// Which of the three things a remark is.
37///
38/// The names are the ones `-fopt-info` uses, in lower case, because the person reading the output
39/// is usually holding a GCC manual.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum Kind {
42 /// The pass rewrote something. This is the only kind that counts as a change.
43 Optimized,
44 /// The pass found a site it could have rewritten and did not. The reason belongs in the text.
45 Missed,
46 /// Something worth saying that is neither, such as what an analysis concluded.
47 Note,
48}
49
50impl Kind {
51 /// The word `-fopt-info` spells this with.
52 #[must_use]
53 pub const fn as_str(self) -> &'static str {
54 match self {
55 Self::Optimized => "optimized",
56 Self::Missed => "missed",
57 Self::Note => "note",
58 }
59 }
60
61 /// The kind that word names, if it names one.
62 #[must_use]
63 pub fn parse(word: &str) -> Option<Self> {
64 match word {
65 "optimized" => Some(Self::Optimized),
66 "missed" => Some(Self::Missed),
67 "note" => Some(Self::Note),
68 _ => None,
69 }
70 }
71
72 /// Every kind, in the order they are printed in.
73 pub const ALL: [Self; 3] = [Self::Optimized, Self::Missed, Self::Note];
74}
75
76impl fmt::Display for Kind {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.write_str(self.as_str())
79 }
80}
81
82/// One named thing a pass did or did not do, and how many times.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct Event {
85 /// Which of the three it is.
86 pub kind: Kind,
87 /// The site, in words, as a thing that happened to one instruction or one loop. Read after a
88 /// count, as in "3 x instruction folded to a constant", so it is singular and has no number
89 /// of its own in it.
90 pub what: &'static str,
91 /// How many times, which is at least one because an event with a count of zero is not
92 /// recorded.
93 pub count: u32,
94}
95
96/// What one pass has to say about one function.
97///
98/// Built by the pass as it works, and read by the pass manager afterwards. The events come out in
99/// the order they were first recorded rather than sorted, so a pass that records its sites in the
100/// order it visits them produces output that reads like the walk.
101#[derive(Debug, Clone, Default, PartialEq, Eq)]
102pub struct Stats {
103 /// One entry per distinct kind and text, in the order the first of each arrived.
104 events: Vec<Event>,
105}
106
107impl Stats {
108 /// Nothing recorded yet, which is what every pass starts from and what a pass that found
109 /// nothing to do ends with.
110 #[must_use]
111 pub const fn new() -> Self {
112 Self { events: Vec::new() }
113 }
114
115 /// Records that the pass rewrote something, once.
116 ///
117 /// Call this at the rewrite and not at the end, because a count kept in a local and written
118 /// once is a count that is wrong on every early return.
119 pub fn optimized(&mut self, what: &'static str) {
120 self.record(Kind::Optimized, what, 1);
121 }
122
123 /// Records that the pass could have rewritten something and did not, once.
124 pub fn missed(&mut self, what: &'static str) {
125 self.record(Kind::Missed, what, 1);
126 }
127
128 /// Records something that is neither a rewrite nor a missed one, once.
129 pub fn note(&mut self, what: &'static str) {
130 self.record(Kind::Note, what, 1);
131 }
132
133 /// Adds `count` to the event with this kind and text, creating it if it is new.
134 ///
135 /// A count of zero does nothing at all, so a pass may add a number it computed without first
136 /// checking whether the number is zero and without producing an event that says a thing
137 /// happened no times.
138 pub fn record(&mut self, kind: Kind, what: &'static str, count: u32) {
139 if count == 0 {
140 return;
141 }
142 match self.events.iter_mut().find(|it| it.kind == kind && it.what == what) {
143 Some(event) => event.count += count,
144 None => self.events.push(Event { kind, what, count }),
145 }
146 }
147
148 /// Takes everything in `other` into this, keeping the order the two of them are already in.
149 ///
150 /// What the pass manager does across the functions of a module, so that the total for a pass
151 /// is one set of named counts rather than one per function.
152 pub fn merge(&mut self, other: &Self) {
153 for event in &other.events {
154 self.record(event.kind, event.what, event.count);
155 }
156 }
157
158 /// Whether the pass changed the function.
159 ///
160 /// One `optimized` event is a change and any number of `missed` and `note` events is not.
161 /// This is the whole reason the record is a return value: the pass manager has no other way
162 /// to find out, so recording the rewrite is not a thing a pass can leave until later.
163 #[must_use]
164 pub fn changed(&self) -> bool {
165 self.events.iter().any(|event| event.kind == Kind::Optimized)
166 }
167
168 /// Whether the pass said anything at all.
169 #[must_use]
170 pub fn is_empty(&self) -> bool {
171 self.events.is_empty()
172 }
173
174 /// Everything recorded, in the order it first arrived.
175 #[must_use]
176 pub fn events(&self) -> &[Event] {
177 &self.events
178 }
179
180 /// Everything of one kind, in the same order.
181 pub fn of(&self, kind: Kind) -> impl Iterator<Item = &Event> {
182 self.events.iter().filter(move |event| event.kind == kind)
183 }
184
185 /// How many times this exact thing was recorded, which is zero when it never was.
186 #[must_use]
187 pub fn count(&self, kind: Kind, what: &str) -> u32 {
188 self.events
189 .iter()
190 .find(|it| it.kind == kind && it.what == what)
191 .map_or(0, |event| event.count)
192 }
193
194 /// How many times anything of this kind was recorded.
195 #[must_use]
196 pub fn total(&self, kind: Kind) -> u32 {
197 self.of(kind).map(|event| event.count).sum()
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::{Kind, Stats};
204
205 #[test]
206 fn a_pass_that_recorded_nothing_changed_nothing() {
207 let stats = Stats::new();
208 assert!(!stats.changed());
209 assert!(stats.is_empty());
210 assert_eq!(stats.total(Kind::Optimized), 0);
211 }
212
213 #[test]
214 fn only_an_optimized_event_is_a_change() {
215 let mut stats = Stats::new();
216 stats.missed("nothing to see");
217 stats.note("an analysis said something");
218 assert!(!stats.changed(), "a miss is not a change");
219 assert!(!stats.is_empty(), "it still said something");
220 stats.optimized("a rewrite");
221 assert!(stats.changed());
222 }
223
224 #[test]
225 fn the_same_event_twice_is_one_event_with_a_count_of_two() {
226 let mut stats = Stats::new();
227 stats.optimized("folded");
228 stats.optimized("folded");
229 stats.optimized("removed");
230 assert_eq!(stats.events().len(), 2);
231 assert_eq!(stats.count(Kind::Optimized, "folded"), 2);
232 assert_eq!(stats.count(Kind::Optimized, "removed"), 1);
233 assert_eq!(stats.total(Kind::Optimized), 3);
234 }
235
236 #[test]
237 fn the_same_words_under_two_kinds_are_two_events() {
238 let mut stats = Stats::new();
239 stats.optimized("folded");
240 stats.missed("folded");
241 assert_eq!(stats.events().len(), 2);
242 assert_eq!(stats.count(Kind::Optimized, "folded"), 1);
243 assert_eq!(stats.count(Kind::Missed, "folded"), 1);
244 }
245
246 #[test]
247 fn recording_a_count_of_zero_does_not_make_an_event() {
248 let mut stats = Stats::new();
249 stats.record(Kind::Optimized, "folded", 0);
250 assert!(stats.is_empty(), "an event saying a thing happened no times");
251 assert!(!stats.changed());
252 }
253
254 #[test]
255 fn events_come_out_in_the_order_they_first_arrived() {
256 let mut stats = Stats::new();
257 stats.missed("first");
258 stats.optimized("second");
259 stats.missed("first");
260 let seen: Vec<&str> = stats.events().iter().map(|event| event.what).collect();
261 assert_eq!(seen, ["first", "second"], "the second `first` moved it");
262 }
263
264 #[test]
265 fn merging_adds_the_counts_and_keeps_the_left_hand_order() {
266 let mut left = Stats::new();
267 left.optimized("folded");
268 left.missed("out of fuel");
269 let mut right = Stats::new();
270 right.optimized("removed");
271 right.optimized("folded");
272 left.merge(&right);
273 let seen: Vec<(&str, u32)> =
274 left.events().iter().map(|event| (event.what, event.count)).collect();
275 assert_eq!(seen, [("folded", 2), ("out of fuel", 1), ("removed", 1)]);
276 }
277
278 #[test]
279 fn merging_an_empty_record_changes_nothing() {
280 let mut stats = Stats::new();
281 stats.optimized("folded");
282 let before = stats.clone();
283 stats.merge(&Stats::new());
284 assert_eq!(stats, before);
285 }
286
287 #[test]
288 fn the_words_are_the_ones_opt_info_uses_and_they_round_trip() {
289 for kind in Kind::ALL {
290 assert_eq!(Kind::parse(kind.as_str()), Some(kind));
291 assert_eq!(kind.to_string(), kind.as_str());
292 }
293 assert_eq!(Kind::parse("all"), None, "`all` is every kind and not one of them");
294 assert_eq!(Kind::parse("Optimized"), None);
295 }
296
297 #[test]
298 fn one_kind_at_a_time_is_in_the_order_it_was_recorded() {
299 let mut stats = Stats::new();
300 stats.missed("a");
301 stats.optimized("b");
302 stats.missed("c");
303 let missed: Vec<&str> = stats.of(Kind::Missed).map(|event| event.what).collect();
304 assert_eq!(missed, ["a", "c"]);
305 assert_eq!(stats.total(Kind::Missed), 2);
306 }
307}