// 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. // // -link exe // Use exe as the path to the cmd/link binary. // // -linkflags 'list' // Pass the space-separated list of flags to the linker. // // -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 linker 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") flagLinker = flag.String("link", "", "use `exe` as the cmd/link binary") flagLinkerFlags = flag.String("linkflags", "", "additional `flags` to pass to link") 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"}}, {"BenchmarkLinkCompiler", link{"cmd/compile", ""}}, {"BenchmarkExternalLinkCompiler", link{"cmd/compile", "-linkmode=external"}}, {"BenchmarkLinkWithoutDebugCompiler", link{"cmd/compile", "-w"}}, {"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 == "" { var foundTool string foundTool, compiler = toolPath("compile", "6g") if foundTool == "6g" { is6g = true } } linker = *flagLinker if linker == "" && !is6g { // TODO: Support 6l _, linker = toolPath("link") } if is6g { *flagMemprofilerate = -1 *flagAlloc = false *flagCpuprofile = "" *flagMemprofile = "" } 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}}, {"BenchmarkPkgLink", link{*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 toolPath(names ...string) (found, path string) { var out1 []byte var err1 error for i, name := range names { out, err := exec.Command(*flagGoCmd, "tool", "-n", name).CombinedOutput() if err == nil { return name, strings.TrimSpace(string(out)) } if i == 0 { out1, err1 = out, err } } log.Fatalf("go tool -n %s: %v\n%s", names[0], err1, out1) return "", "" } type Pkg struct { Dir string GoFiles []string } func goList(dir string) (*Pkg, error) { var pkg Pkg out, err := exec.Command(*flagGoCmd, "list", "-json", dir).Output() if err != nil { return nil, fmt.Errorf("go list -json %s: %v", dir, err) } if err := json.Unmarshal(out, &pkg); err != nil { return nil, fmt.Errorf("go list -json %s: unmarshal: %v", dir, err) } return &pkg, nil } 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. pkg, err := goList(c.dir) if err != nil { return err } args := []string{"-o", "_compilebench_.o"} args = append(args, strings.Fields(*flagCompilerFlags)...) args = append(args, pkg.GoFiles...) if err := runBuildCmd(name, count, pkg.Dir, compiler, args); err != nil { return err } 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 } type link struct{ dir, flags string } func (link) long() bool { return false } func (r link) run(name string, count int) error { if linker == "" { // No linker. Skip the test. return nil } // Build dependencies. out, err := exec.Command(*flagGoCmd, "build", "-i", "-o", "/dev/null", r.dir).CombinedOutput() if err != nil { return fmt.Errorf("go build -i %s: %v\n%s", r.dir, err, out) } // Build the main package. pkg, err := goList(r.dir) if err != nil { return err } args := []string{"-o", "_compilebench_.o"} args = append(args, pkg.GoFiles...) cmd := exec.Command(compiler, args...) cmd.Dir = pkg.Dir cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { return fmt.Errorf("compiling: %v", err) } defer os.Remove(pkg.Dir + "/_compilebench_.o") // Link the main package. args = []string{"-o", "_compilebench_.exe"} args = append(args, strings.Fields(*flagLinkerFlags)...) args = append(args, strings.Fields(r.flags)...) args = append(args, "_compilebench_.o") if err := runBuildCmd(name, count, pkg.Dir, linker, args); err != nil { return err } fmt.Println() defer os.Remove(pkg.Dir + "/_compilebench_.exe") return err } // runBuildCmd runs "tool args..." in dir, measures standard build // tool metrics, and prints a benchmark line. The caller may print // additional metrics and then must print a newline. // // This assumes tool accepts standard build tool flags like // -memprofilerate, -memprofile, and -cpuprofile. func runBuildCmd(name string, count int, dir, tool string, args []string) error { var preArgs []string if *flagMemprofilerate >= 0 { preArgs = append(preArgs, "-memprofilerate", fmt.Sprint(*flagMemprofilerate)) } if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" { if *flagAlloc || *flagMemprofile != "" { preArgs = append(preArgs, "-memprofile", "_compilebench_.memprof") } if *flagCpuprofile != "" { preArgs = append(preArgs, "-cpuprofile", "_compilebench_.cpuprof") } } cmd := exec.Command(tool, append(preArgs, args...)...) cmd.Dir = dir cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr start := time.Now() err := cmd.Run() if err != nil { return err } end := time.Now() haveAllocs, haveRSS := false, false var allocs, allocbytes, rssbytes int64 if *flagAlloc || *flagMemprofile != "" { out, err := ioutil.ReadFile(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 } haveAllocs = true switch f[1] { case "TotalAlloc": allocbytes = val case "Mallocs": allocs = val case "MaxRSS": haveRSS = true rssbytes = val } } if !haveAllocs { log.Println("missing stats in memprof (golang.org/issue/18641)") } 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(dir + "/_compilebench_.memprof") } if *flagCpuprofile != "" { out, err := ioutil.ReadFile(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(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 haveAllocs { fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs) } if haveRSS { fmt.Printf(" %d maxRSS/op", rssbytes) } return nil }