Source file
src/testing/benchmark.go
Documentation: testing
1
2
3
4
5 package testing
6
7 import (
8 "flag"
9 "fmt"
10 "internal/race"
11 "io"
12 "math"
13 "os"
14 "runtime"
15 "sort"
16 "strconv"
17 "strings"
18 "sync"
19 "sync/atomic"
20 "time"
21 "unicode"
22 )
23
24 func initBenchmarkFlags() {
25 matchBenchmarks = flag.String("test.bench", "", "run only benchmarks matching `regexp`")
26 benchmarkMemory = flag.Bool("test.benchmem", false, "print memory allocations for benchmarks")
27 flag.Var(&benchTime, "test.benchtime", "run each benchmark for duration `d`")
28 }
29
30 var (
31 matchBenchmarks *string
32 benchmarkMemory *bool
33
34 benchTime = benchTimeFlag{d: 1 * time.Second}
35 )
36
37 type benchTimeFlag struct {
38 d time.Duration
39 n int
40 }
41
42 func (f *benchTimeFlag) String() string {
43 if f.n > 0 {
44 return fmt.Sprintf("%dx", f.n)
45 }
46 return time.Duration(f.d).String()
47 }
48
49 func (f *benchTimeFlag) Set(s string) error {
50 if strings.HasSuffix(s, "x") {
51 n, err := strconv.ParseInt(s[:len(s)-1], 10, 0)
52 if err != nil || n <= 0 {
53 return fmt.Errorf("invalid count")
54 }
55 *f = benchTimeFlag{n: int(n)}
56 return nil
57 }
58 d, err := time.ParseDuration(s)
59 if err != nil || d <= 0 {
60 return fmt.Errorf("invalid duration")
61 }
62 *f = benchTimeFlag{d: d}
63 return nil
64 }
65
66
67 var benchmarkLock sync.Mutex
68
69
70 var memStats runtime.MemStats
71
72
73
74 type InternalBenchmark struct {
75 Name string
76 F func(b *B)
77 }
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 type B struct {
93 common
94 importPath string
95 context *benchContext
96 N int
97 previousN int
98 previousDuration time.Duration
99 benchFunc func(b *B)
100 benchTime benchTimeFlag
101 bytes int64
102 missingBytes bool
103 timerOn bool
104 showAllocResult bool
105 result BenchmarkResult
106 parallelism int
107
108 startAllocs uint64
109 startBytes uint64
110
111 netAllocs uint64
112 netBytes uint64
113
114 extra map[string]float64
115 }
116
117
118
119
120 func (b *B) StartTimer() {
121 if !b.timerOn {
122 runtime.ReadMemStats(&memStats)
123 b.startAllocs = memStats.Mallocs
124 b.startBytes = memStats.TotalAlloc
125 b.start = time.Now()
126 b.timerOn = true
127 }
128 }
129
130
131
132
133 func (b *B) StopTimer() {
134 if b.timerOn {
135 b.duration += time.Since(b.start)
136 runtime.ReadMemStats(&memStats)
137 b.netAllocs += memStats.Mallocs - b.startAllocs
138 b.netBytes += memStats.TotalAlloc - b.startBytes
139 b.timerOn = false
140 }
141 }
142
143
144
145
146 func (b *B) ResetTimer() {
147 if b.extra == nil {
148
149
150 b.extra = make(map[string]float64, 16)
151 } else {
152 for k := range b.extra {
153 delete(b.extra, k)
154 }
155 }
156 if b.timerOn {
157 runtime.ReadMemStats(&memStats)
158 b.startAllocs = memStats.Mallocs
159 b.startBytes = memStats.TotalAlloc
160 b.start = time.Now()
161 }
162 b.duration = 0
163 b.netAllocs = 0
164 b.netBytes = 0
165 }
166
167
168
169 func (b *B) SetBytes(n int64) { b.bytes = n }
170
171
172
173
174 func (b *B) ReportAllocs() {
175 b.showAllocResult = true
176 }
177
178
179 func (b *B) runN(n int) {
180 benchmarkLock.Lock()
181 defer benchmarkLock.Unlock()
182 defer b.runCleanup(normalPanic)
183
184
185 runtime.GC()
186 b.raceErrors = -race.Errors()
187 b.N = n
188 b.parallelism = 1
189 b.ResetTimer()
190 b.StartTimer()
191 b.benchFunc(b)
192 b.StopTimer()
193 b.previousN = n
194 b.previousDuration = b.duration
195 b.raceErrors += race.Errors()
196 if b.raceErrors > 0 {
197 b.Errorf("race detected during execution of benchmark")
198 }
199 }
200
201 func min(x, y int64) int64 {
202 if x > y {
203 return y
204 }
205 return x
206 }
207
208 func max(x, y int64) int64 {
209 if x < y {
210 return y
211 }
212 return x
213 }
214
215
216
217 func (b *B) run1() bool {
218 if ctx := b.context; ctx != nil {
219
220 if n := len(b.name) + ctx.extLen + 1; n > ctx.maxLen {
221 ctx.maxLen = n + 8
222 }
223 }
224 go func() {
225
226
227 defer func() {
228 b.signal <- true
229 }()
230
231 b.runN(1)
232 }()
233 <-b.signal
234 if b.failed {
235 fmt.Fprintf(b.w, "--- FAIL: %s\n%s", b.name, b.output)
236 return false
237 }
238
239
240 if atomic.LoadInt32(&b.hasSub) != 0 || b.finished {
241 tag := "BENCH"
242 if b.skipped {
243 tag = "SKIP"
244 }
245 if b.chatty != nil && (len(b.output) > 0 || b.finished) {
246 b.trimOutput()
247 fmt.Fprintf(b.w, "--- %s: %s\n%s", tag, b.name, b.output)
248 }
249 return false
250 }
251 return true
252 }
253
254 var labelsOnce sync.Once
255
256
257
258 func (b *B) run() {
259 labelsOnce.Do(func() {
260 fmt.Fprintf(b.w, "goos: %s\n", runtime.GOOS)
261 fmt.Fprintf(b.w, "goarch: %s\n", runtime.GOARCH)
262 if b.importPath != "" {
263 fmt.Fprintf(b.w, "pkg: %s\n", b.importPath)
264 }
265 })
266 if b.context != nil {
267
268 b.context.processBench(b)
269 } else {
270
271 b.doBench()
272 }
273 }
274
275 func (b *B) doBench() BenchmarkResult {
276 go b.launch()
277 <-b.signal
278 return b.result
279 }
280
281
282
283
284
285 func (b *B) launch() {
286
287
288 defer func() {
289 b.signal <- true
290 }()
291
292
293 if b.benchTime.n > 0 {
294 b.runN(b.benchTime.n)
295 } else {
296 d := b.benchTime.d
297 for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
298 last := n
299
300 goalns := d.Nanoseconds()
301 prevIters := int64(b.N)
302 prevns := b.duration.Nanoseconds()
303 if prevns <= 0 {
304
305 prevns = 1
306 }
307
308
309
310
311
312 n = goalns * prevIters / prevns
313
314 n += n / 5
315
316 n = min(n, 100*last)
317
318 n = max(n, last+1)
319
320 n = min(n, 1e9)
321 b.runN(int(n))
322 }
323 }
324 b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.extra}
325 }
326
327
328
329
330
331
332
333
334
335
336 func (b *B) ReportMetric(n float64, unit string) {
337 if unit == "" {
338 panic("metric unit must not be empty")
339 }
340 if strings.IndexFunc(unit, unicode.IsSpace) >= 0 {
341 panic("metric unit must not contain whitespace")
342 }
343 b.extra[unit] = n
344 }
345
346
347 type BenchmarkResult struct {
348 N int
349 T time.Duration
350 Bytes int64
351 MemAllocs uint64
352 MemBytes uint64
353
354
355 Extra map[string]float64
356 }
357
358
359 func (r BenchmarkResult) NsPerOp() int64 {
360 if v, ok := r.Extra["ns/op"]; ok {
361 return int64(v)
362 }
363 if r.N <= 0 {
364 return 0
365 }
366 return r.T.Nanoseconds() / int64(r.N)
367 }
368
369
370 func (r BenchmarkResult) mbPerSec() float64 {
371 if v, ok := r.Extra["MB/s"]; ok {
372 return v
373 }
374 if r.Bytes <= 0 || r.T <= 0 || r.N <= 0 {
375 return 0
376 }
377 return (float64(r.Bytes) * float64(r.N) / 1e6) / r.T.Seconds()
378 }
379
380
381
382 func (r BenchmarkResult) AllocsPerOp() int64 {
383 if v, ok := r.Extra["allocs/op"]; ok {
384 return int64(v)
385 }
386 if r.N <= 0 {
387 return 0
388 }
389 return int64(r.MemAllocs) / int64(r.N)
390 }
391
392
393
394 func (r BenchmarkResult) AllocedBytesPerOp() int64 {
395 if v, ok := r.Extra["B/op"]; ok {
396 return int64(v)
397 }
398 if r.N <= 0 {
399 return 0
400 }
401 return int64(r.MemBytes) / int64(r.N)
402 }
403
404
405
406
407
408
409
410
411 func (r BenchmarkResult) String() string {
412 buf := new(strings.Builder)
413 fmt.Fprintf(buf, "%8d", r.N)
414
415
416 ns, ok := r.Extra["ns/op"]
417 if !ok {
418 ns = float64(r.T.Nanoseconds()) / float64(r.N)
419 }
420 if ns != 0 {
421 buf.WriteByte('\t')
422 prettyPrint(buf, ns, "ns/op")
423 }
424
425 if mbs := r.mbPerSec(); mbs != 0 {
426 fmt.Fprintf(buf, "\t%7.2f MB/s", mbs)
427 }
428
429
430
431 var extraKeys []string
432 for k := range r.Extra {
433 switch k {
434 case "ns/op", "MB/s", "B/op", "allocs/op":
435
436 continue
437 }
438 extraKeys = append(extraKeys, k)
439 }
440 sort.Strings(extraKeys)
441 for _, k := range extraKeys {
442 buf.WriteByte('\t')
443 prettyPrint(buf, r.Extra[k], k)
444 }
445 return buf.String()
446 }
447
448 func prettyPrint(w io.Writer, x float64, unit string) {
449
450
451 var format string
452 switch y := math.Abs(x); {
453 case y == 0 || y >= 99.95:
454 format = "%10.0f %s"
455 case y >= 9.995:
456 format = "%12.1f %s"
457 case y >= 0.9995:
458 format = "%13.2f %s"
459 case y >= 0.09995:
460 format = "%14.3f %s"
461 case y >= 0.009995:
462 format = "%15.4f %s"
463 case y >= 0.0009995:
464 format = "%16.5f %s"
465 default:
466 format = "%17.6f %s"
467 }
468 fmt.Fprintf(w, format, x, unit)
469 }
470
471
472 func (r BenchmarkResult) MemString() string {
473 return fmt.Sprintf("%8d B/op\t%8d allocs/op",
474 r.AllocedBytesPerOp(), r.AllocsPerOp())
475 }
476
477
478 func benchmarkName(name string, n int) string {
479 if n != 1 {
480 return fmt.Sprintf("%s-%d", name, n)
481 }
482 return name
483 }
484
485 type benchContext struct {
486 match *matcher
487
488 maxLen int
489 extLen int
490 }
491
492
493
494 func RunBenchmarks(matchString func(pat, str string) (bool, error), benchmarks []InternalBenchmark) {
495 runBenchmarks("", matchString, benchmarks)
496 }
497
498 func runBenchmarks(importPath string, matchString func(pat, str string) (bool, error), benchmarks []InternalBenchmark) bool {
499
500 if len(*matchBenchmarks) == 0 {
501 return true
502 }
503
504 maxprocs := 1
505 for _, procs := range cpuList {
506 if procs > maxprocs {
507 maxprocs = procs
508 }
509 }
510 ctx := &benchContext{
511 match: newMatcher(matchString, *matchBenchmarks, "-test.bench"),
512 extLen: len(benchmarkName("", maxprocs)),
513 }
514 var bs []InternalBenchmark
515 for _, Benchmark := range benchmarks {
516 if _, matched, _ := ctx.match.fullName(nil, Benchmark.Name); matched {
517 bs = append(bs, Benchmark)
518 benchName := benchmarkName(Benchmark.Name, maxprocs)
519 if l := len(benchName) + ctx.extLen + 1; l > ctx.maxLen {
520 ctx.maxLen = l
521 }
522 }
523 }
524 main := &B{
525 common: common{
526 name: "Main",
527 w: os.Stdout,
528 bench: true,
529 },
530 importPath: importPath,
531 benchFunc: func(b *B) {
532 for _, Benchmark := range bs {
533 b.Run(Benchmark.Name, Benchmark.F)
534 }
535 },
536 benchTime: benchTime,
537 context: ctx,
538 }
539 if Verbose() {
540 main.chatty = newChattyPrinter(main.w)
541 }
542 main.runN(1)
543 return !main.failed
544 }
545
546
547 func (ctx *benchContext) processBench(b *B) {
548 for i, procs := range cpuList {
549 for j := uint(0); j < *count; j++ {
550 runtime.GOMAXPROCS(procs)
551 benchName := benchmarkName(b.name, procs)
552
553
554 if b.chatty == nil {
555 fmt.Fprintf(b.w, "%-*s\t", ctx.maxLen, benchName)
556 }
557
558 if i > 0 || j > 0 {
559 b = &B{
560 common: common{
561 signal: make(chan bool),
562 name: b.name,
563 w: b.w,
564 chatty: b.chatty,
565 bench: true,
566 },
567 benchFunc: b.benchFunc,
568 benchTime: b.benchTime,
569 }
570 b.run1()
571 }
572 r := b.doBench()
573 if b.failed {
574
575
576
577 fmt.Fprintf(b.w, "--- FAIL: %s\n%s", benchName, b.output)
578 continue
579 }
580 results := r.String()
581 if b.chatty != nil {
582 fmt.Fprintf(b.w, "%-*s\t", ctx.maxLen, benchName)
583 }
584 if *benchmarkMemory || b.showAllocResult {
585 results += "\t" + r.MemString()
586 }
587 fmt.Fprintln(b.w, results)
588
589
590 if len(b.output) > 0 {
591 b.trimOutput()
592 fmt.Fprintf(b.w, "--- BENCH: %s\n%s", benchName, b.output)
593 }
594 if p := runtime.GOMAXPROCS(-1); p != procs {
595 fmt.Fprintf(os.Stderr, "testing: %s left GOMAXPROCS set to %d\n", benchName, p)
596 }
597 }
598 }
599 }
600
601
602
603
604
605
606 func (b *B) Run(name string, f func(b *B)) bool {
607
608
609 atomic.StoreInt32(&b.hasSub, 1)
610 benchmarkLock.Unlock()
611 defer benchmarkLock.Lock()
612
613 benchName, ok, partial := b.name, true, false
614 if b.context != nil {
615 benchName, ok, partial = b.context.match.fullName(&b.common, name)
616 }
617 if !ok {
618 return true
619 }
620 var pc [maxStackLen]uintptr
621 n := runtime.Callers(2, pc[:])
622 sub := &B{
623 common: common{
624 signal: make(chan bool),
625 name: benchName,
626 parent: &b.common,
627 level: b.level + 1,
628 creator: pc[:n],
629 w: b.w,
630 chatty: b.chatty,
631 bench: true,
632 },
633 importPath: b.importPath,
634 benchFunc: f,
635 benchTime: b.benchTime,
636 context: b.context,
637 }
638 if partial {
639
640
641 atomic.StoreInt32(&sub.hasSub, 1)
642 }
643
644 if b.chatty != nil {
645 labelsOnce.Do(func() {
646 fmt.Printf("goos: %s\n", runtime.GOOS)
647 fmt.Printf("goarch: %s\n", runtime.GOARCH)
648 if b.importPath != "" {
649 fmt.Printf("pkg: %s\n", b.importPath)
650 }
651 })
652
653 fmt.Println(benchName)
654 }
655
656 if sub.run1() {
657 sub.run()
658 }
659 b.add(sub.result)
660 return !sub.failed
661 }
662
663
664
665
666 func (b *B) add(other BenchmarkResult) {
667 r := &b.result
668
669
670 r.N = 1
671 r.T += time.Duration(other.NsPerOp())
672 if other.Bytes == 0 {
673
674
675 b.missingBytes = true
676 r.Bytes = 0
677 }
678 if !b.missingBytes {
679 r.Bytes += other.Bytes
680 }
681 r.MemAllocs += uint64(other.AllocsPerOp())
682 r.MemBytes += uint64(other.AllocedBytesPerOp())
683 }
684
685
686 func (b *B) trimOutput() {
687
688
689
690 const maxNewlines = 10
691 for nlCount, j := 0, 0; j < len(b.output); j++ {
692 if b.output[j] == '\n' {
693 nlCount++
694 if nlCount >= maxNewlines {
695 b.output = append(b.output[:j], "\n\t... [output truncated]\n"...)
696 break
697 }
698 }
699 }
700 }
701
702
703 type PB struct {
704 globalN *uint64
705 grain uint64
706 cache uint64
707 bN uint64
708 }
709
710
711 func (pb *PB) Next() bool {
712 if pb.cache == 0 {
713 n := atomic.AddUint64(pb.globalN, pb.grain)
714 if n <= pb.bN {
715 pb.cache = pb.grain
716 } else if n < pb.bN+pb.grain {
717 pb.cache = pb.bN + pb.grain - n
718 } else {
719 return false
720 }
721 }
722 pb.cache--
723 return true
724 }
725
726
727
728
729
730
731
732
733
734
735
736 func (b *B) RunParallel(body func(*PB)) {
737 if b.N == 0 {
738 return
739 }
740
741
742
743 grain := uint64(0)
744 if b.previousN > 0 && b.previousDuration > 0 {
745 grain = 1e5 * uint64(b.previousN) / uint64(b.previousDuration)
746 }
747 if grain < 1 {
748 grain = 1
749 }
750
751
752 if grain > 1e4 {
753 grain = 1e4
754 }
755
756 n := uint64(0)
757 numProcs := b.parallelism * runtime.GOMAXPROCS(0)
758 var wg sync.WaitGroup
759 wg.Add(numProcs)
760 for p := 0; p < numProcs; p++ {
761 go func() {
762 defer wg.Done()
763 pb := &PB{
764 globalN: &n,
765 grain: grain,
766 bN: uint64(b.N),
767 }
768 body(pb)
769 }()
770 }
771 wg.Wait()
772 if n <= uint64(b.N) && !b.Failed() {
773 b.Fatal("RunParallel: body exited without pb.Next() == false")
774 }
775 }
776
777
778
779
780 func (b *B) SetParallelism(p int) {
781 if p >= 1 {
782 b.parallelism = p
783 }
784 }
785
786
787
788
789
790
791
792
793
794 func Benchmark(f func(b *B)) BenchmarkResult {
795 b := &B{
796 common: common{
797 signal: make(chan bool),
798 w: discard{},
799 },
800 benchFunc: f,
801 benchTime: benchTime,
802 }
803 if b.run1() {
804 b.run()
805 }
806 return b.result
807 }
808
809 type discard struct{}
810
811 func (discard) Write(b []byte) (n int, err error) { return len(b), nil }
812
View as plain text