Skip to main content

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}