Skip to content

Commit 03a79e9

Browse files
committed
testing: stop rounding b.N
The original goal of rounding to readable b.N was to make it easier to eyeball times. However, proper analysis requires tooling (such as benchstat) anyway. Instead, take b.N as it comes. This will reduce the impact of external noise such as GC on benchmarks. This requires reworking our iteration estimates. We used to calculate the estimated ns/op and then divide our target ns by that estimate. However, this order of operations was destructive when the ns/op was very small; rounding could hide almost an order of magnitude of variation. Instead, multiply first, then divide. Also, make n an int64 to avoid overflow. Prior to this change, we attempted to cap b.N at 1e9. Due to rounding up, it was possible to get b.N as high as 2e9. This change consistently enforces the 1e9 cap. This change also reduces the wall time required to run benchmarks. Here's the impact of this change on the wall time to run all benchmarks once with benchtime=1s on some std packages: name old time/op new time/op delta bytes 306s ± 1% 238s ± 1% -22.24% (p=0.000 n=10+10) encoding/json 112s ± 8% 99s ± 7% -11.64% (p=0.000 n=10+10) net/http 54.7s ± 7% 44.9s ± 4% -17.94% (p=0.000 n=10+9) runtime 957s ± 1% 714s ± 0% -25.38% (p=0.000 n=10+9) strings 262s ± 1% 201s ± 1% -23.27% (p=0.000 n=10+10) [Geo mean] 216s 172s -20.23% Updates #24735 Change-Id: I7e38efb8e23c804046bf4fc065b3f5f3991d0a15 Reviewed-on: https://go-review.googlesource.com/c/go/+/112155 Reviewed-by: Austin Clements <[email protected]>
1 parent 3023d7d commit 03a79e9

File tree

4 files changed

+24
-108
lines changed

4 files changed

+24
-108
lines changed

src/cmd/go/go_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4947,14 +4947,14 @@ func TestTestRegexps(t *testing.T) {
49474947
x_test.go:15: LOG: Y running N=10000
49484948
x_test.go:15: LOG: Y running N=1000000
49494949
x_test.go:15: LOG: Y running N=100000000
4950-
x_test.go:15: LOG: Y running N=2000000000
4950+
x_test.go:15: LOG: Y running N=1000000000
49514951
--- BENCH: BenchmarkX/Y
49524952
x_test.go:15: LOG: Y running N=1
49534953
x_test.go:15: LOG: Y running N=100
49544954
x_test.go:15: LOG: Y running N=10000
49554955
x_test.go:15: LOG: Y running N=1000000
49564956
x_test.go:15: LOG: Y running N=100000000
4957-
x_test.go:15: LOG: Y running N=2000000000
4957+
x_test.go:15: LOG: Y running N=1000000000
49584958
--- BENCH: BenchmarkX
49594959
x_test.go:13: LOG: X running N=1
49604960
--- BENCH: BenchmarkXX

src/testing/benchmark.go

+21-50
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,6 @@ func (b *B) ReportAllocs() {
170170
b.showAllocResult = true
171171
}
172172

173-
func (b *B) nsPerOp() int64 {
174-
if b.N <= 0 {
175-
return 0
176-
}
177-
return b.duration.Nanoseconds() / int64(b.N)
178-
}
179-
180173
// runN runs a single benchmark for the specified number of iterations.
181174
func (b *B) runN(n int) {
182175
benchmarkLock.Lock()
@@ -199,53 +192,20 @@ func (b *B) runN(n int) {
199192
}
200193
}
201194

202-
func min(x, y int) int {
195+
func min(x, y int64) int64 {
203196
if x > y {
204197
return y
205198
}
206199
return x
207200
}
208201

209-
func max(x, y int) int {
202+
func max(x, y int64) int64 {
210203
if x < y {
211204
return y
212205
}
213206
return x
214207
}
215208

216-
// roundDown10 rounds a number down to the nearest power of 10.
217-
func roundDown10(n int) int {
218-
var tens = 0
219-
// tens = floor(log_10(n))
220-
for n >= 10 {
221-
n = n / 10
222-
tens++
223-
}
224-
// result = 10^tens
225-
result := 1
226-
for i := 0; i < tens; i++ {
227-
result *= 10
228-
}
229-
return result
230-
}
231-
232-
// roundUp rounds x up to a number of the form [1eX, 2eX, 3eX, 5eX].
233-
func roundUp(n int) int {
234-
base := roundDown10(n)
235-
switch {
236-
case n <= base:
237-
return base
238-
case n <= (2 * base):
239-
return 2 * base
240-
case n <= (3 * base):
241-
return 3 * base
242-
case n <= (5 * base):
243-
return 5 * base
244-
default:
245-
return 10 * base
246-
}
247-
}
248-
249209
// run1 runs the first iteration of benchFunc. It reports whether more
250210
// iterations of this benchmarks should be run.
251211
func (b *B) run1() bool {
@@ -328,20 +288,31 @@ func (b *B) launch() {
328288
b.runN(b.benchTime.n)
329289
} else {
330290
d := b.benchTime.d
331-
for n := 1; !b.failed && b.duration < d && n < 1e9; {
291+
for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
332292
last := n
333293
// Predict required iterations.
334-
n = int(d.Nanoseconds())
335-
if nsop := b.nsPerOp(); nsop != 0 {
336-
n /= int(nsop)
294+
goalns := d.Nanoseconds()
295+
prevIters := int64(b.N)
296+
prevns := b.duration.Nanoseconds()
297+
if prevns <= 0 {
298+
// Round up, to avoid div by zero.
299+
prevns = 1
337300
}
301+
// Order of operations matters.
302+
// For very fast benchmarks, prevIters ~= prevns.
303+
// If you divide first, you get 0 or 1,
304+
// which can hide an order of magnitude in execution time.
305+
// So multiply first, then divide.
306+
n = goalns * prevIters / prevns
338307
// Run more iterations than we think we'll need (1.2x).
308+
n += n / 5
339309
// Don't grow too fast in case we had timing errors previously.
310+
n = min(n, 100*last)
340311
// Be sure to run at least one more than last time.
341-
n = max(min(n+n/5, 100*last), last+1)
342-
// Round up to something easy to read.
343-
n = roundUp(n)
344-
b.runN(n)
312+
n = max(n, last+1)
313+
// Don't run more than 1e9 times. (This also keeps n in int range on 32 bit platforms.)
314+
n = min(n, 1e9)
315+
b.runN(int(n))
345316
}
346317
}
347318
b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.extra}

src/testing/benchmark_test.go

-51
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,6 @@ import (
1414
"text/template"
1515
)
1616

17-
var roundDownTests = []struct {
18-
v, expected int
19-
}{
20-
{1, 1},
21-
{9, 1},
22-
{10, 10},
23-
{11, 10},
24-
{100, 100},
25-
{101, 100},
26-
{999, 100},
27-
{1000, 1000},
28-
{1001, 1000},
29-
}
30-
31-
func TestRoundDown10(t *testing.T) {
32-
for _, tt := range roundDownTests {
33-
actual := testing.RoundDown10(tt.v)
34-
if tt.expected != actual {
35-
t.Errorf("roundDown10(%d): expected %d, actual %d", tt.v, tt.expected, actual)
36-
}
37-
}
38-
}
39-
40-
var roundUpTests = []struct {
41-
v, expected int
42-
}{
43-
{0, 1},
44-
{1, 1},
45-
{2, 2},
46-
{3, 3},
47-
{5, 5},
48-
{9, 10},
49-
{999, 1000},
50-
{1000, 1000},
51-
{1400, 2000},
52-
{1700, 2000},
53-
{2700, 3000},
54-
{4999, 5000},
55-
{5000, 5000},
56-
{5001, 10000},
57-
}
58-
59-
func TestRoundUp(t *testing.T) {
60-
for _, tt := range roundUpTests {
61-
actual := testing.RoundUp(tt.v)
62-
if tt.expected != actual {
63-
t.Errorf("roundUp(%d): expected %d, actual %d", tt.v, tt.expected, actual)
64-
}
65-
}
66-
}
67-
6817
var prettyPrintTests = []struct {
6918
v float64
7019
expected string

src/testing/export_test.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,4 @@
44

55
package testing
66

7-
var (
8-
RoundDown10 = roundDown10
9-
RoundUp = roundUp
10-
PrettyPrint = prettyPrint
11-
)
7+
var PrettyPrint = prettyPrint

0 commit comments

Comments
 (0)