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