1
0
mirror of https://github.com/golang/go synced 2024-11-18 14:14:46 -07:00
go/cmd/compilebench/main.go
Austin Clements 8a42e17289 compilebench: clean up different benchmark types
compilebench contains three different types of benchmarks and we're
about to add a fourth. Currently we dispatch based on name. This CL
cleans this up so the list of benchmarks points to how to run each
benchmark. It also joins together common error-reporting paths.

Change-Id: Icd4f77bdda9776863792dcd85f27b17829a833e6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/175799
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
2019-05-09 00:13:06 +00:00

401 lines
11 KiB
Go

// Copyright 2015 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.
// Compilebench benchmarks the speed of the Go compiler.
//
// Usage:
//
// compilebench [options]
//
// It times the compilation of various packages and prints results in
// the format used by package testing (and expected by golang.org/x/perf/cmd/benchstat).
//
// The options are:
//
// -alloc
// Report allocations.
//
// -compile exe
// Use exe as the path to the cmd/compile binary.
//
// -compileflags 'list'
// Pass the space-separated list of flags to the compilation.
//
// -count n
// Run each benchmark n times (default 1).
//
// -cpuprofile file
// Write a CPU profile of the compiler to file.
//
// -go path
// Path to "go" command (default "go").
//
// -memprofile file
// Write a memory profile of the compiler to file.
//
// -memprofilerate rate
// Set runtime.MemProfileRate during compilation.
//
// -obj
// Report object file statistics.
//
// -pkg pkg
// Benchmark compiling a single package.
//
// -run regexp
// Only run benchmarks with names matching regexp.
//
// -short
// Skip long-running benchmarks.
//
// Although -cpuprofile and -memprofile are intended to write a
// combined profile for all the executed benchmarks to file,
// today they write only the profile for the last benchmark executed.
//
// The default memory profiling rate is one profile sample per 512 kB
// allocated (see ``go doc runtime.MemProfileRate'').
// Lowering the rate (for example, -memprofilerate 64000) produces
// a more fine-grained and therefore accurate profile, but it also incurs
// execution cost. For benchmark comparisons, never use timings
// obtained with a low -memprofilerate option.
//
// Example
//
// Assuming the base version of the compiler has been saved with
// ``toolstash save,'' this sequence compares the old and new compiler:
//
// compilebench -count 10 -compile $(toolstash -n compile) >old.txt
// compilebench -count 10 >new.txt
// benchstat old.txt new.txt
//
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
var (
goroot string
compiler string
runRE *regexp.Regexp
is6g bool
)
var (
flagGoCmd = flag.String("go", "go", "path to \"go\" command")
flagAlloc = flag.Bool("alloc", false, "report allocations")
flagObj = flag.Bool("obj", false, "report object file stats")
flagCompiler = flag.String("compile", "", "use `exe` as the cmd/compile binary")
flagCompilerFlags = flag.String("compileflags", "", "additional `flags` to pass to compile")
flagRun = flag.String("run", "", "run benchmarks matching `regexp`")
flagCount = flag.Int("count", 1, "run benchmarks `n` times")
flagCpuprofile = flag.String("cpuprofile", "", "write CPU profile to `file`")
flagMemprofile = flag.String("memprofile", "", "write memory profile to `file`")
flagMemprofilerate = flag.Int64("memprofilerate", -1, "set memory profile `rate`")
flagPackage = flag.String("pkg", "", "if set, benchmark the package at path `pkg`")
flagShort = flag.Bool("short", false, "skip long-running benchmarks")
)
type test struct {
name string
r runner
}
type runner interface {
long() bool
run(name string, count int) error
}
var tests = []test{
{"BenchmarkTemplate", compile{"html/template"}},
{"BenchmarkUnicode", compile{"unicode"}},
{"BenchmarkGoTypes", compile{"go/types"}},
{"BenchmarkCompiler", compile{"cmd/compile/internal/gc"}},
{"BenchmarkSSA", compile{"cmd/compile/internal/ssa"}},
{"BenchmarkFlate", compile{"compress/flate"}},
{"BenchmarkGoParser", compile{"go/parser"}},
{"BenchmarkReflect", compile{"reflect"}},
{"BenchmarkTar", compile{"archive/tar"}},
{"BenchmarkXML", compile{"encoding/xml"}},
{"BenchmarkStdCmd", goBuild{[]string{"std", "cmd"}}},
{"BenchmarkHelloSize", size{"$GOROOT/test/helloworld.go", false}},
{"BenchmarkCmdGoSize", size{"cmd/go", true}},
}
func usage() {
fmt.Fprintf(os.Stderr, "usage: compilebench [options]\n")
fmt.Fprintf(os.Stderr, "options:\n")
flag.PrintDefaults()
os.Exit(2)
}
func main() {
log.SetFlags(0)
log.SetPrefix("compilebench: ")
flag.Usage = usage
flag.Parse()
if flag.NArg() != 0 {
usage()
}
s, err := exec.Command(*flagGoCmd, "env", "GOROOT").CombinedOutput()
if err != nil {
log.Fatalf("%s env GOROOT: %v", *flagGoCmd, err)
}
goroot = strings.TrimSpace(string(s))
os.Setenv("GOROOT", goroot) // for any subcommands
compiler = *flagCompiler
if compiler == "" {
out, err := exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
if err != nil {
out, err = exec.Command(*flagGoCmd, "tool", "-n", "6g").CombinedOutput()
is6g = true
if err != nil {
out, err = exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
log.Fatalf("go tool -n compiler: %v\n%s", err, out)
}
}
compiler = strings.TrimSpace(string(out))
}
if *flagRun != "" {
r, err := regexp.Compile(*flagRun)
if err != nil {
log.Fatalf("invalid -run argument: %v", err)
}
runRE = r
}
if *flagPackage != "" {
tests = []test{
{"BenchmarkPkg", compile{*flagPackage}},
}
runRE = nil
}
for i := 0; i < *flagCount; i++ {
for _, tt := range tests {
if tt.r.long() && *flagShort {
continue
}
if runRE == nil || runRE.MatchString(tt.name) {
if err := tt.r.run(tt.name, i); err != nil {
log.Printf("%s: %v", tt.name, err)
}
}
}
}
}
func runCmd(name string, cmd *exec.Cmd) error {
start := time.Now()
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v\n%s", err, out)
}
fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds())
return nil
}
type goBuild struct{ pkgs []string }
func (goBuild) long() bool { return true }
func (r goBuild) run(name string, count int) error {
args := []string{"build", "-a"}
if *flagCompilerFlags != "" {
args = append(args, "-gcflags", *flagCompilerFlags)
}
args = append(args, r.pkgs...)
cmd := exec.Command(*flagGoCmd, args...)
cmd.Dir = filepath.Join(goroot, "src")
return runCmd(name, cmd)
}
type size struct {
// path is either a path to a file ("$GOROOT/test/helloworld.go") or a package path ("cmd/go").
path string
isLong bool
}
func (r size) long() bool { return r.isLong }
func (r size) run(name string, count int) error {
if strings.HasPrefix(r.path, "$GOROOT/") {
r.path = goroot + "/" + r.path[len("$GOROOT/"):]
}
cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", r.path)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
defer os.Remove("_compilebenchout_")
info, err := os.Stat("_compilebenchout_")
if err != nil {
return err
}
out, err := exec.Command("size", "_compilebenchout_").CombinedOutput()
if err != nil {
return fmt.Errorf("size: %v\n%s", err, out)
}
lines := strings.Split(string(out), "\n")
if len(lines) < 2 {
return fmt.Errorf("not enough output from size: %s", out)
}
f := strings.Fields(lines[1])
if strings.HasPrefix(lines[0], "__TEXT") && len(f) >= 2 { // OS X
fmt.Printf("%s 1 %s text-bytes %s data-bytes %v exe-bytes\n", name, f[0], f[1], info.Size())
} else if strings.Contains(lines[0], "bss") && len(f) >= 3 {
fmt.Printf("%s 1 %s text-bytes %s data-bytes %s bss-bytes %v exe-bytes\n", name, f[0], f[1], f[2], info.Size())
}
return nil
}
type compile struct{ dir string }
func (compile) long() bool { return false }
func (c compile) run(name string, count int) error {
// Make sure dependencies needed by go tool compile are installed to GOROOT/pkg.
out, err := exec.Command(*flagGoCmd, "build", "-i", c.dir).CombinedOutput()
if err != nil {
return fmt.Errorf("go build -i %s: %v\n%s", c.dir, err, out)
}
// Find dir and source file list.
var pkg struct {
Dir string
GoFiles []string
}
out, err = exec.Command(*flagGoCmd, "list", "-json", c.dir).Output()
if err != nil {
return fmt.Errorf("go list -json %s: %v\n", c.dir, err)
}
if err := json.Unmarshal(out, &pkg); err != nil {
return fmt.Errorf("go list -json %s: unmarshal: %v", c.dir, err)
}
args := []string{"-o", "_compilebench_.o"}
if is6g {
*flagMemprofilerate = -1
*flagAlloc = false
*flagCpuprofile = ""
*flagMemprofile = ""
}
if *flagMemprofilerate >= 0 {
args = append(args, "-memprofilerate", fmt.Sprint(*flagMemprofilerate))
}
args = append(args, strings.Fields(*flagCompilerFlags)...)
if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" {
if *flagAlloc || *flagMemprofile != "" {
args = append(args, "-memprofile", "_compilebench_.memprof")
}
if *flagCpuprofile != "" {
args = append(args, "-cpuprofile", "_compilebench_.cpuprof")
}
}
args = append(args, pkg.GoFiles...)
cmd := exec.Command(compiler, args...)
cmd.Dir = pkg.Dir
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
start := time.Now()
err = cmd.Run()
if err != nil {
return err
}
end := time.Now()
var allocs, allocbytes int64
if *flagAlloc || *flagMemprofile != "" {
out, err := ioutil.ReadFile(pkg.Dir + "/_compilebench_.memprof")
if err != nil {
log.Print("cannot find memory profile after compilation")
}
for _, line := range strings.Split(string(out), "\n") {
f := strings.Fields(line)
if len(f) < 4 || f[0] != "#" || f[2] != "=" {
continue
}
val, err := strconv.ParseInt(f[3], 0, 64)
if err != nil {
continue
}
switch f[1] {
case "TotalAlloc":
allocbytes = val
case "Mallocs":
allocs = val
}
}
if *flagMemprofile != "" {
outpath := *flagMemprofile
if *flagCount != 1 {
outpath = fmt.Sprintf("%s_%d", outpath, count)
}
if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
log.Print(err)
}
}
os.Remove(pkg.Dir + "/_compilebench_.memprof")
}
if *flagCpuprofile != "" {
out, err := ioutil.ReadFile(pkg.Dir + "/_compilebench_.cpuprof")
if err != nil {
log.Print(err)
}
outpath := *flagCpuprofile
if *flagCount != 1 {
outpath = fmt.Sprintf("%s_%d", outpath, count)
}
if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
log.Print(err)
}
os.Remove(pkg.Dir + "/_compilebench_.cpuprof")
}
wallns := end.Sub(start).Nanoseconds()
userns := cmd.ProcessState.UserTime().Nanoseconds()
fmt.Printf("%s 1 %d ns/op %d user-ns/op", name, wallns, userns)
if *flagAlloc {
fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs)
}
opath := pkg.Dir + "/_compilebench_.o"
if *flagObj {
// TODO(josharian): object files are big; just read enough to find what we seek.
data, err := ioutil.ReadFile(opath)
if err != nil {
log.Print(err)
}
// Find start of export data.
i := bytes.Index(data, []byte("\n$$B\n")) + len("\n$$B\n")
// Count bytes to end of export data.
nexport := bytes.Index(data[i:], []byte("\n$$\n"))
fmt.Printf(" %d object-bytes %d export-bytes", len(data), nexport)
}
fmt.Println()
os.Remove(opath)
return nil
}