| // Copyright 2022 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package benchmath | |
| import ( | |
| "fmt" | |
| "math" | |
| "sync" | |
| "github.com/aclements/go-moremath/stats" | |
| ) | |
| // AssumeNothing is a non-parametric Assumption (that is, it makes no | |
| // distributional assumptions). The summary statistic is the sample | |
| // median and comparisons are done using the Mann-Whitney U test. | |
| // | |
| // This is a good default assumption for benchmarks. | |
| // There's substantial evidence that benchmark results are non-normal. | |
| // The disadvantage (of any non-parametric methods) is that this is | |
| // less statistically powerful than parametric methods. | |
| var AssumeNothing = assumeNothing{} | |
| type assumeNothing struct{} | |
| var _ Assumption = assumeNothing{} | |
| // medianCache maps from ciKey to stats.QuantileCIResult for median | |
| // confidence intervals. | |
| var medianCache sync.Map | |
| func medianCI(n int, confidence float64) stats.QuantileCIResult { | |
| type ciKey struct { | |
| n int | |
| confidence float64 | |
| } | |
| key := ciKey{n, confidence} | |
| if ciX, ok := medianCache.Load(key); ok { | |
| return ciX.(stats.QuantileCIResult) | |
| } | |
| ci := stats.QuantileCI(n, 0.5, confidence) | |
| medianCache.Store(key, ci) | |
| return ci | |
| } | |
| // medianSamples returns the minimum number of samples required to get | |
| // a finite confidence interval at the given confidence level. | |
| func medianSamples(confidence float64) (op string, n int) { | |
| const limit = 50 | |
| // We need at least two samples to have an interval. | |
| for n = 2; n <= limit; n++ { | |
| ci := medianCI(n, confidence) | |
| if 0 < ci.LoOrder && ci.HiOrder <= n { | |
| return ">=", n | |
| } | |
| } | |
| return ">", limit | |
| } | |
| func (assumeNothing) SummaryLabel() string { | |
| return "median" | |
| } | |
| func (assumeNothing) Summary(s *Sample, confidence float64) Summary { | |
| ci := medianCI(len(s.Values), confidence) | |
| median, lo, hi := ci.SampleCI(s.sample()) | |
| var warnings []error | |
| if math.IsInf(lo, 0) || math.IsInf(hi, 0) { | |
| // Explain to the user why there's a ±∞ | |
| op, need := medianSamples(confidence) | |
| msg := fmt.Errorf("need %s %d samples for confidence interval at level %v", op, need, confidence) | |
| warnings = append(warnings, msg) | |
| } | |
| return Summary{median, lo, hi, ci.Confidence, warnings} | |
| } | |
| // uTestMinP[n] is the minimum possible P value for the U-test with | |
| // two samples of size n. | |
| // | |
| // Generated by go run mktables.go. | |
| var uTestMinP = []float64{ | |
| 1: 1, | |
| 2: 0.3333333333333333, | |
| 3: 0.1, | |
| 4: 0.02857142857142857, | |
| 5: 0.007936507936507936, | |
| 6: 0.0021645021645021645, | |
| 7: 0.0005827505827505828, | |
| 8: 0.0001554001554001554, | |
| 9: 4.113533525298231e-05, | |
| } | |
| // uTestSamples returns the minimum number of samples required for the | |
| // U-test to achieve statistical significance at the given alpha | |
| // level. | |
| func uTestSamples(alpha float64) (op string, n int) { | |
| for n, minP := range uTestMinP { | |
| if n == 0 { | |
| continue | |
| } | |
| if minP <= alpha { | |
| return ">=", n | |
| } | |
| } | |
| return ">", len(uTestMinP) | |
| } | |
| func (assumeNothing) Compare(s1, s2 *Sample) Comparison { | |
| res, err := stats.MannWhitneyUTest(s1.Values, s2.Values, stats.LocationDiffers) | |
| if err != nil { | |
| // The U-test failed. Report as if there's no | |
| // significant difference, along with the error. | |
| return Comparison{P: 1, N1: len(s1.Values), N2: len(s2.Values), Alpha: s1.Thresholds.CompareAlpha, Warnings: []error{err}} | |
| } | |
| cmp := Comparison{P: res.P, N1: res.N1, N2: res.N2, Alpha: s1.Thresholds.CompareAlpha} | |
| // Warn if there aren't enough samples to report a difference | |
| // even if they were maximally diverged. | |
| if cmp.P > cmp.Alpha { | |
| op, n := uTestSamples(cmp.Alpha) | |
| if cmp.N1 < n && cmp.N2 < n { | |
| // We could deal with asymmetric sample sizes | |
| // by first ramping up the smaller sample | |
| // until the minimum P value is sufficient or | |
| // the sample sizes are equal. But it doesn't | |
| // seem worth the complexity. | |
| msg := fmt.Errorf("need %s %d samples to detect a difference at alpha level %v", op, n, cmp.Alpha) | |
| cmp.Warnings = append(cmp.Warnings, msg) | |
| } | |
| } | |
| return cmp | |
| } |