--- /dev/null
+// 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
+}