Skip to content

Commit 083497d

Browse files
committed
Generalize effect_size::calculate to two or more engines
Instead of requiring exactly two engines, produce an `EffectSize` for each pair of engines present in each group of measurements. Fewer than two engines is still an error.
1 parent e9dda0a commit 083497d

1 file changed

Lines changed: 112 additions & 40 deletions

File tree

crates/analysis/src/effect_size.rs

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ use anyhow::Result;
33
use sightglass_data::{EffectSize, Engine, Measurement, Phase, Summary};
44
use std::{collections::BTreeSet, io::Write};
55

6-
/// Find the effect size (and confidence interval) of between two different
7-
/// engines (i.e. two different commits of Wasmtime).
6+
/// Find the effect size (and confidence interval) between different engines
7+
/// (i.e. different commits of Wasmtime).
88
///
99
/// This allows us to justify statements like "we are 99% confident that the new
1010
/// register allocator is 13.6% faster (± 1.7%) than the old register
1111
/// allocator."
1212
///
13-
/// This can only test differences between the results for exactly two different
14-
/// engines. If there aren't exactly two different engines represented in
13+
/// The `measurements` must contain results for two or more different engines.
14+
/// Each returned [`EffectSize`] compares exactly two different engines: for
15+
/// every group of measurements that share an architecture, benchmark, phase,
16+
/// and event, one `EffectSize` is produced for each pair of engines present in
17+
/// that group. If fewer than two different engines are represented in
1518
/// `measurements` then an error is returned.
1619
pub fn calculate<'a>(
1720
significance_level: f64,
@@ -24,52 +27,58 @@ pub fn calculate<'a>(
2427
Found {significance_level}."
2528
);
2629

30+
// We need at least two different engines to have anything to compare.
31+
let all_engines: BTreeSet<_> = measurements.iter().map(|m| &m.engine).collect();
32+
anyhow::ensure!(
33+
all_engines.len() >= 2,
34+
"Comparing effect sizes requires two or more different engines. Found {} \
35+
different engines.",
36+
all_engines.len()
37+
);
38+
2739
let keys = KeyBuilder::all()
2840
.engine(false)
2941
.engine_flags(false)
3042
.keys(measurements);
31-
let mut results = Vec::with_capacity(keys.len());
43+
let mut results = Vec::new();
3244

3345
for key in keys {
3446
let key_measurements: Vec<_> = measurements.iter().filter(|m| key.matches(m)).collect();
3547

36-
// NB: `BTreeSet` so they're always sorted.
37-
let engines: BTreeSet<_> = key_measurements.iter().map(|m| &m.engine).collect();
38-
anyhow::ensure!(
39-
engines.len() == 2,
40-
"Can only test significance between exactly two different engines. Found {} \
41-
different engines.",
42-
engines.len()
43-
);
48+
let mut engines: Vec<_> = key_measurements.iter().map(|m| &m.engine).collect();
49+
engines.sort();
4450

45-
let mut engines = engines.into_iter();
46-
let engine_a = engines.next().unwrap();
47-
let engine_b = engines.next().unwrap();
51+
// Create an `EffectSize` comparing each pair of distinct engines within
52+
// this group of measurements.
53+
for (i, engine_a) in engines.iter().enumerate() {
54+
for engine_b in &engines[i + 1..] {
55+
let a: behrens_fisher::Stats = key_measurements
56+
.iter()
57+
.filter(|m| &m.engine == *engine_a)
58+
.map(|m| m.count as f64)
59+
.collect();
60+
let b: behrens_fisher::Stats = key_measurements
61+
.iter()
62+
.filter(|m| &m.engine == *engine_b)
63+
.map(|m| m.count as f64)
64+
.collect();
4865

49-
let a: behrens_fisher::Stats = key_measurements
50-
.iter()
51-
.filter(|m| &m.engine == engine_a)
52-
.map(|m| m.count as f64)
53-
.collect();
54-
let b: behrens_fisher::Stats = key_measurements
55-
.iter()
56-
.filter(|m| &m.engine == engine_b)
57-
.map(|m| m.count as f64)
58-
.collect();
59-
60-
let ci = behrens_fisher::confidence_interval(1.0 - significance_level, a, b).unwrap_or(0.0);
61-
results.push(EffectSize {
62-
arch: key.arch.unwrap(),
63-
wasm: key.wasm.unwrap(),
64-
phase: key.phase.unwrap(),
65-
event: key.event.unwrap(),
66-
a_engine: engine_a.clone(),
67-
a_mean: a.mean,
68-
b_engine: engine_b.clone(),
69-
b_mean: b.mean,
70-
significance_level,
71-
half_width_confidence_interval: ci,
72-
});
66+
let ci = behrens_fisher::confidence_interval(1.0 - significance_level, a, b)
67+
.unwrap_or(0.0);
68+
results.push(EffectSize {
69+
arch: key.arch.clone().unwrap(),
70+
wasm: key.wasm.clone().unwrap(),
71+
phase: key.phase.unwrap(),
72+
event: key.event.clone().unwrap(),
73+
a_engine: (*engine_a).clone(),
74+
a_mean: a.mean,
75+
b_engine: (*engine_b).clone(),
76+
b_mean: b.mean,
77+
significance_level,
78+
half_width_confidence_interval: ci,
79+
});
80+
}
81+
}
7382
}
7483

7584
Ok(results)
@@ -182,3 +191,66 @@ pub fn write(
182191

183192
Ok(())
184193
}
194+
195+
#[cfg(test)]
196+
mod tests {
197+
use super::*;
198+
199+
fn measurement<'a>(engine: &'a str, count: u64) -> Measurement<'a> {
200+
Measurement {
201+
arch: "x86_64".into(),
202+
engine: Engine {
203+
name: engine.into(),
204+
flags: None,
205+
},
206+
wasm: "bench.wasm".into(),
207+
process: 0,
208+
iteration: 0,
209+
phase: Phase::Execution,
210+
event: "cycles".into(),
211+
count,
212+
}
213+
}
214+
215+
#[test]
216+
fn effect_size_for_each_pair_of_engines() -> Result<()> {
217+
// Three engines within a single (arch, wasm, phase, event) group should
218+
// yield one `EffectSize` per unordered pair of engines: (a, b), (a, c),
219+
// and (b, c).
220+
let mut measurements = vec![];
221+
for (engine, base) in [("a", 100), ("b", 200), ("c", 300)] {
222+
for i in 0..5 {
223+
measurements.push(measurement(engine, base + i));
224+
}
225+
}
226+
227+
let effect_sizes = calculate(0.01, &measurements)?;
228+
assert_eq!(effect_sizes.len(), 3);
229+
230+
let pairs: Vec<(String, String)> = effect_sizes
231+
.iter()
232+
.map(|e| (e.a_engine.name.to_string(), e.b_engine.name.to_string()))
233+
.collect();
234+
assert_eq!(
235+
pairs,
236+
vec![
237+
("a".to_string(), "b".to_string()),
238+
("a".to_string(), "c".to_string()),
239+
("b".to_string(), "c".to_string()),
240+
]
241+
);
242+
243+
// Every `EffectSize` compares two *different* engines.
244+
for e in &effect_sizes {
245+
assert_ne!(e.a_engine, e.b_engine);
246+
}
247+
248+
Ok(())
249+
}
250+
251+
#[test]
252+
fn effect_size_requires_at_least_two_engines() {
253+
let measurements = vec![measurement("only", 1), measurement("only", 2)];
254+
assert!(calculate(0.01, &measurements).is_err());
255+
}
256+
}

0 commit comments

Comments
 (0)