Skip to content

Commit 5918101

Browse files
aclementsgopherbot
authored andcommitted
testing: detect a stopped timer in B.Loop
Currently, if the user stops the timer in a B.Loop benchmark loop, the benchmark will run until it hits the timeout and fails. Fix this by detecting that the timer is stopped and failing the benchmark right away. We avoid making the fast path more expensive for this check by "poisoning" the B.Loop iteration counter when the timer is stopped so that it falls back to the slow path, which can check the timer. This causes b to escape from B.Loop, which is totally harmless because it was already definitely heap-allocated. But it causes the test/inline_testingbloop.go errorcheck test to fail. I don't think the escape messages actually mattered to that test, they just had to be matched. To fix this, we drop the debug level to -m=1, since -m=2 prints a lot of extra information for escaping parameters that we don't want to deal with, and change one error check to allow b to escape. Fixes #72971. Change-Id: I7d4abbb1ec1e096685514536f91ba0d581cca6b7 Reviewed-on: https://go-review.googlesource.com/c/go/+/659657 Auto-Submit: Austin Clements <[email protected]> Reviewed-by: Junyang Shao <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent b7f8c2a commit 5918101

File tree

5 files changed

+80
-12
lines changed

5 files changed

+80
-12
lines changed

src/cmd/compile/internal/inline/interleaved/interleaved.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func (s *inlClosureState) mark(n ir.Node) ir.Node {
253253

254254
if isTestingBLoop(n) {
255255
// No inlining nor devirtualization performed on b.Loop body
256-
if base.Flag.LowerM > 1 {
256+
if base.Flag.LowerM > 0 {
257257
fmt.Printf("%v: skip inlining within testing.B.loop for %v\n", ir.Line(n), n)
258258
}
259259
// We still want to explore inlining opportunities in other parts of ForStmt.

src/cmd/compile/internal/test/inl_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ func TestIntendedInlining(t *testing.T) {
230230
"(*Pointer[go.shape.int]).Store",
231231
"(*Pointer[go.shape.int]).Swap",
232232
},
233+
"testing": {
234+
"(*B).Loop",
235+
},
233236
}
234237

235238
if !goexperiment.SwissMap {

src/testing/benchmark.go

+51-9
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,13 @@ type B struct {
120120
// n is the target number of iterations. It gets bumped up as we go.
121121
// When the benchmark loop is done, we commit this to b.N so users can
122122
// do reporting based on it, but we avoid exposing it until then.
123-
n int
123+
n uint64
124124
// i is the current Loop iteration. It's strictly monotonically
125125
// increasing toward n.
126-
i int
126+
//
127+
// The high bit is used to poison the Loop fast path and fall back to
128+
// the slow path.
129+
i uint64
127130

128131
done bool // set when B.Loop return false
129132
}
@@ -139,6 +142,7 @@ func (b *B) StartTimer() {
139142
b.startBytes = memStats.TotalAlloc
140143
b.start = highPrecisionTimeNow()
141144
b.timerOn = true
145+
b.loop.i &^= loopPoisonTimer
142146
}
143147
}
144148

@@ -151,6 +155,8 @@ func (b *B) StopTimer() {
151155
b.netAllocs += memStats.Mallocs - b.startAllocs
152156
b.netBytes += memStats.TotalAlloc - b.startBytes
153157
b.timerOn = false
158+
// If we hit B.Loop with the timer stopped, fail.
159+
b.loop.i |= loopPoisonTimer
154160
}
155161
}
156162

@@ -388,19 +394,32 @@ func (b *B) stopOrScaleBLoop() bool {
388394
// Stop the timer so we don't count cleanup time
389395
b.StopTimer()
390396
// Commit iteration count
391-
b.N = b.loop.n
397+
b.N = int(b.loop.n)
392398
b.loop.done = true
393399
return false
394400
}
395401
// Loop scaling
396402
goalns := b.benchTime.d.Nanoseconds()
397403
prevIters := int64(b.loop.n)
398-
b.loop.n = predictN(goalns, prevIters, t.Nanoseconds(), prevIters)
404+
b.loop.n = uint64(predictN(goalns, prevIters, t.Nanoseconds(), prevIters))
405+
if b.loop.n&loopPoisonMask != 0 {
406+
// The iteration count should never get this high, but if it did we'd be
407+
// in big trouble.
408+
panic("loop iteration target overflow")
409+
}
399410
b.loop.i++
400411
return true
401412
}
402413

403414
func (b *B) loopSlowPath() bool {
415+
// Consistency checks
416+
if !b.timerOn {
417+
b.Fatal("B.Loop called with timer stopped")
418+
}
419+
if b.loop.i&loopPoisonMask != 0 {
420+
panic(fmt.Sprintf("unknown loop stop condition: %#x", b.loop.i))
421+
}
422+
404423
if b.loop.n == 0 {
405424
// If it's the first call to b.Loop() in the benchmark function.
406425
// Allows more precise measurement of benchmark loop cost counts.
@@ -414,14 +433,14 @@ func (b *B) loopSlowPath() bool {
414433
}
415434
// Handles fixed iterations case
416435
if b.benchTime.n > 0 {
417-
if b.loop.n < b.benchTime.n {
418-
b.loop.n = b.benchTime.n
436+
if b.loop.n < uint64(b.benchTime.n) {
437+
b.loop.n = uint64(b.benchTime.n)
419438
b.loop.i++
420439
return true
421440
}
422441
b.StopTimer()
423442
// Commit iteration count
424-
b.N = b.loop.n
443+
b.N = int(b.loop.n)
425444
b.loop.done = true
426445
return false
427446
}
@@ -463,15 +482,38 @@ func (b *B) loopSlowPath() bool {
463482
// whereas b.N-based benchmarks must run the benchmark function (and any
464483
// associated setup and cleanup) several times.
465484
func (b *B) Loop() bool {
466-
// On the first call, both i and n are 0, so we'll fall through to the slow
467-
// path in that case, too.
485+
// This is written such that the fast path is as fast as possible and can be
486+
// inlined.
487+
//
488+
// There are three cases where we'll fall out of the fast path:
489+
//
490+
// - On the first call, both i and n are 0.
491+
//
492+
// - If the loop reaches the n'th iteration, then i == n and we need
493+
// to figure out the new target iteration count or if we're done.
494+
//
495+
// - If the timer is stopped, it poisons the top bit of i so the slow
496+
// path can do consistency checks and fail.
468497
if b.loop.i < b.loop.n {
469498
b.loop.i++
470499
return true
471500
}
472501
return b.loopSlowPath()
473502
}
474503

504+
// The loopPoison constants can be OR'd into B.loop.i to cause it to fall back
505+
// to the slow path.
506+
const (
507+
loopPoisonTimer = uint64(1 << (63 - iota))
508+
// If necessary, add more poison bits here.
509+
510+
// loopPoisonMask is the set of all loop poison bits. (iota-1) is the index
511+
// of the bit we just set, from which we recreate that bit mask. We subtract
512+
// 1 to set all of the bits below that bit, then complement the result to
513+
// get the mask. Sorry, not sorry.
514+
loopPoisonMask = ^uint64((1 << (63 - (iota - 1))) - 1)
515+
)
516+
475517
// BenchmarkResult contains the results of a benchmark run.
476518
type BenchmarkResult struct {
477519
N int // The number of iterations.

src/testing/loop_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,26 @@ func TestBenchmarkBLoopError(t *T) {
129129
t.Errorf("want N == 0, got %d", bRet.N)
130130
}
131131
}
132+
133+
func TestBenchmarkBLoopStop(t *T) {
134+
var bState *B
135+
var bLog bytes.Buffer
136+
bRet := Benchmark(func(b *B) {
137+
bState = b
138+
b.common.w = &bLog
139+
140+
for i := 0; b.Loop(); i++ {
141+
b.StopTimer()
142+
}
143+
})
144+
if !bState.failed {
145+
t.Errorf("benchmark should have failed")
146+
}
147+
const wantLog = "B.Loop called with timer stopped"
148+
if log := bLog.String(); !strings.Contains(log, wantLog) {
149+
t.Errorf("missing error %q in output:\n%s", wantLog, log)
150+
}
151+
if bRet.N != 0 {
152+
t.Errorf("want N == 0, got %d", bRet.N)
153+
}
154+
}

test/inline_testingbloop.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// errorcheck -0 -m=2
1+
// errorcheck -0 -m
22

33
// Copyright 2024 The Go Authors. All rights reserved.
44
// Use of this source code is governed by a BSD-style
@@ -15,7 +15,7 @@ func caninline(x int) int { // ERROR "can inline caninline"
1515
return x
1616
}
1717

18-
func cannotinline(b *testing.B) { // ERROR "b does not escape" "cannot inline cannotinline.*"
18+
func test(b *testing.B) { // ERROR "leaking param: b"
1919
for i := 0; i < b.N; i++ {
2020
caninline(1) // ERROR "inlining call to caninline"
2121
}

0 commit comments

Comments
 (0)