// Copyright 2014 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "flag" "fmt" "os" "sort" "strconv" "text/tabwriter" "golang.org/x/tools/benchmark/parse" ) var ( changedOnly = flag.Bool("changed", false, "show only benchmarks that have changed") magSort = flag.Bool("mag", false, "sort benchmarks by magnitude of change") best = flag.Bool("best", false, "compare best times from old and new") ) const usageFooter = ` Each input file should be from: go test -run=NONE -bench=. > [old,new].txt Benchcmp compares old and new for each benchmark. If -test.benchmem=true is added to the "go test" command benchcmp will also compare memory allocations. ` func main() { fmt.Fprintf(os.Stderr, "benchcmp is deprecated in favor of benchstat: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat\n") flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s old.txt new.txt\n\n", os.Args[0]) flag.PrintDefaults() fmt.Fprint(os.Stderr, usageFooter) os.Exit(2) } flag.Parse() if flag.NArg() != 2 { flag.Usage() } before := parseFile(flag.Arg(0)) after := parseFile(flag.Arg(1)) cmps, warnings := Correlate(before, after) for _, warn := range warnings { fmt.Fprintln(os.Stderr, warn) } if len(cmps) == 0 { fatal("benchcmp: no repeated benchmarks") } w := new(tabwriter.Writer) w.Init(os.Stdout, 0, 0, 5, ' ', 0) defer w.Flush() var header bool // Has the header has been displayed yet for a given block? if *magSort { sort.Sort(ByDeltaNsPerOp(cmps)) } else { sort.Sort(ByParseOrder(cmps)) } for _, cmp := range cmps { if !cmp.Measured(parse.NsPerOp) { continue } if delta := cmp.DeltaNsPerOp(); !*changedOnly || delta.Changed() { if !header { fmt.Fprint(w, "benchmark\told ns/op\tnew ns/op\tdelta\n") header = true } fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", cmp.Name(), formatNs(cmp.Before.NsPerOp), formatNs(cmp.After.NsPerOp), delta.Percent()) } } header = false if *magSort { sort.Sort(ByDeltaMBPerS(cmps)) } for _, cmp := range cmps { if !cmp.Measured(parse.MBPerS) { continue } if delta := cmp.DeltaMBPerS(); !*changedOnly || delta.Changed() { if !header { fmt.Fprint(w, "\nbenchmark\told MB/s\tnew MB/s\tspeedup\n") header = true } fmt.Fprintf(w, "%s\t%.2f\t%.2f\t%s\n", cmp.Name(), cmp.Before.MBPerS, cmp.After.MBPerS, delta.Multiple()) } } header = false if *magSort { sort.Sort(ByDeltaAllocsPerOp(cmps)) } for _, cmp := range cmps { if !cmp.Measured(parse.AllocsPerOp) { continue } if delta := cmp.DeltaAllocsPerOp(); !*changedOnly || delta.Changed() { if !header { fmt.Fprint(w, "\nbenchmark\told allocs\tnew allocs\tdelta\n") header = true } fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocsPerOp, cmp.After.AllocsPerOp, delta.Percent()) } } header = false if *magSort { sort.Sort(ByDeltaAllocedBytesPerOp(cmps)) } for _, cmp := range cmps { if !cmp.Measured(parse.AllocedBytesPerOp) { continue } if delta := cmp.DeltaAllocedBytesPerOp(); !*changedOnly || delta.Changed() { if !header { fmt.Fprint(w, "\nbenchmark\told bytes\tnew bytes\tdelta\n") header = true } fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocedBytesPerOp, cmp.After.AllocedBytesPerOp, cmp.DeltaAllocedBytesPerOp().Percent()) } } } func fatal(msg interface{}) { fmt.Fprintln(os.Stderr, msg) os.Exit(1) } func parseFile(path string) parse.Set { f, err := os.Open(path) if err != nil { fatal(err) } defer f.Close() bb, err := parse.ParseSet(f) if err != nil { fatal(err) } if *best { selectBest(bb) } return bb } func selectBest(bs parse.Set) { for name, bb := range bs { if len(bb) < 2 { continue } ord := bb[0].Ord best := bb[0] for _, b := range bb { if b.NsPerOp < best.NsPerOp { b.Ord = ord best = b } } bs[name] = []*parse.Benchmark{best} } } // formatNs formats ns measurements to expose a useful amount of // precision. It mirrors the ns precision logic of testing.B. func formatNs(ns float64) string { prec := 0 switch { case ns < 10: prec = 2 case ns < 100: prec = 1 } return strconv.FormatFloat(ns, 'f', prec, 64) }