Skip to content

Commit 0e1b989

Browse files
dfinkelneild
authored andcommitted
testing: exit B.Loop early upon saturation
There's a cap of 1 billion benchmark iterations because more than that is usually not going to give more useful data. Unfortunately, the existing implementation neglected to check whether the 1e9 cap had already been exceeded when it adjusted the number of iterations in the B.Loop slow path (stopOrScaleBLoop), since it's only when that cap is hit that it needed to terminate early. As a result, for _very_ cheap benchmarks (e.g. testing assembly implementations with just a few instructions), the B.Loop would stop incrementing the number of iterations, but wouldn't terminate early, making it re-enter the slow-path _every_ iteration until the benchmark time was exhausted. This wasn't normally visible with the default -benchtime 2s, but when raised to 5s, it would cause benchmarks that took <5ns/op to be reported as exactly 5ns/op. (which looks a bit suspicious) Notably, one can use -count for larger groupings to compute statistics. golang.org/x/perf/cmd/benchstat is valuable for coalescing larger run-counts from -count into more useful statistics. Add a test which allows for fewer iterations on slow/contended platforms but guards against reintroducing a bug of this nature. Fixes golang#75210 Change-Id: Ie7f0b2e6c737b064448434f3ed565bfef8c4f020 Reviewed-on: https://go-review.googlesource.com/c/go/+/700275 Reviewed-by: Junyang Shao <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Sean Liao <[email protected]> Auto-Submit: Sean Liao <[email protected]>
1 parent 84e9ab3 commit 0e1b989

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

src/testing/benchmark.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ func (b *B) doBench() BenchmarkResult {
298298
return b.result
299299
}
300300

301+
// Don't run more than 1e9 times. (This also keeps n in int range on 32 bit platforms.)
302+
const maxBenchPredictIters = 1_000_000_000
303+
301304
func predictN(goalns int64, prevIters int64, prevns int64, last int64) int {
302305
if prevns == 0 {
303306
// Round up to dodge divide by zero. See https://go.dev/issue/70709.
@@ -317,7 +320,7 @@ func predictN(goalns int64, prevIters int64, prevns int64, last int64) int {
317320
// Be sure to run at least one more than last time.
318321
n = max(n, last+1)
319322
// Don't run more than 1e9 times. (This also keeps n in int range on 32 bit platforms.)
320-
n = min(n, 1e9)
323+
n = min(n, maxBenchPredictIters)
321324
return int(n)
322325
}
323326

@@ -403,7 +406,9 @@ func (b *B) stopOrScaleBLoop() bool {
403406
// in big trouble.
404407
panic("loop iteration target overflow")
405408
}
406-
return true
409+
// predictN may have capped the number of iterations; make sure to
410+
// terminate if we've already hit that cap.
411+
return uint64(prevIters) < b.loop.n
407412
}
408413

409414
func (b *B) loopSlowPath() bool {

src/testing/loop_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package testing
77
import (
88
"bytes"
99
"strings"
10+
"time"
1011
)
1112

1213
// See also TestBenchmarkBLoop* in other files.
@@ -75,6 +76,42 @@ func TestBenchmarkBLoop(t *T) {
7576
}
7677
}
7778

79+
func TestBenchmarkBLoopCheapEarlyTerminate(t *T) {
80+
if Short() {
81+
t.Skip("B.Loop test needs to run for > 1s to saturate 1e9 iterations")
82+
}
83+
runCnt := 0
84+
// Set the benchmark time high enough that we're likely to hit the 1B
85+
// iteration limit even on very slow hardware.
86+
// (on an AMD Ryzen 5900X, this benchmark runs in just over a second)
87+
//
88+
// Notably, the assertions below shouldn't fail if a test-run is slow
89+
// enough that it doesn't saturate the limit.
90+
const maxBenchTime = time.Second * 30
91+
res := Benchmark(func(b *B) {
92+
// Set the benchmark time _much_ higher than required to hit 1e9 iterations.
93+
b.benchTime.d = maxBenchTime
94+
for b.Loop() {
95+
runCnt++
96+
}
97+
})
98+
if runCnt > maxBenchPredictIters {
99+
t.Errorf("loop body ran more than max (%d) times: %d", maxBenchPredictIters, runCnt)
100+
if res.T >= maxBenchTime {
101+
t.Logf("cheap benchmark exhausted time budget: %s; ran for %s", maxBenchTime, res.T)
102+
}
103+
}
104+
105+
if res.N != runCnt {
106+
t.Errorf("disagreeing loop counts: res.N reported %d, while b.Loop() iterated %d times", res.N, runCnt)
107+
}
108+
109+
if res.N > maxBenchPredictIters {
110+
t.Errorf("benchmark result claims more runs than max (%d) times: %d", maxBenchPredictIters, res.N)
111+
}
112+
113+
}
114+
78115
func TestBenchmarkBLoopBreak(t *T) {
79116
var bState *B
80117
var bLog bytes.Buffer

0 commit comments

Comments
 (0)