// 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 rsc.io/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. // // -memprofile file // Write a memory profile of the compiler to file. // // -memprofilerate rate // Set runtime.MemProfileRate during compilation. // // -run regexp // Only run benchmarks with names matching regexp. // // 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 ( "flag" "fmt" "go/build" "io/ioutil" "log" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" ) var ( goroot = runtime.GOROOT() compiler string runRE *regexp.Regexp is6g bool ) var ( flagAlloc = flag.Bool("alloc", false, "report allocations") 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`") flagShort = flag.Bool("short", false, "skip long-running benchmarks") ) var tests = []struct { name string dir string long bool }{ {"BenchmarkTemplate", "html/template", false}, {"BenchmarkUnicode", "unicode", false}, {"BenchmarkGoTypes", "go/types", false}, {"BenchmarkCompiler", "cmd/compile/internal/gc", false}, {"BenchmarkSSA", "cmd/compile/internal/ssa", false}, {"BenchmarkFlate", "compress/flate", false}, {"BenchmarkGoParser", "go/parser", false}, {"BenchmarkReflect", "reflect", false}, {"BenchmarkTar", "archive/tar", false}, {"BenchmarkXML", "encoding/xml", false}, {"BenchmarkStdCmd", "", true}, {"BenchmarkHelloSize", "", false}, {"BenchmarkCmdGoSize", "", 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() } compiler = *flagCompiler if compiler == "" { out, err := exec.Command("go", "tool", "-n", "compile").CombinedOutput() if err != nil { out, err = exec.Command("go", "tool", "-n", "6g").CombinedOutput() is6g = true if err != nil { out, err = exec.Command("go", "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 } for i := 0; i < *flagCount; i++ { for _, tt := range tests { if tt.long && *flagShort { continue } if runRE == nil || runRE.MatchString(tt.name) { runBuild(tt.name, tt.dir) } } } } func runCmd(name string, cmd *exec.Cmd) { start := time.Now() out, err := cmd.CombinedOutput() if err != nil { log.Printf("%v: %v\n%s", name, err, out) return } fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds()) } func runStdCmd() { cmd := exec.Command("go", "build", "-a", "std", "cmd") cmd.Dir = filepath.Join(runtime.GOROOT(), "src") runCmd("BenchmarkStdCmd", cmd) } func runCmdGoSize() { runSize("BenchmarkCmdGoSize", filepath.Join(runtime.GOROOT(), "bin/go")) } func runHelloSize() { cmd := exec.Command("go", "build", "-o", "_hello_", filepath.Join(runtime.GOROOT(), "test/helloworld.go")) cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Print(err) return } defer os.Remove("_hello_") runSize("BenchmarkHelloSize", "_hello_") } func runSize(name, file string) { info, err := os.Stat(file) if err != nil { log.Print(err) return } out, err := exec.Command("size", file).CombinedOutput() if err != nil { log.Printf("size: %v\n%s", err, out) return } lines := strings.Split(string(out), "\n") if len(lines) < 2 { log.Printf("not enough output from size: %s", out) return } 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()) } } func runBuild(name, dir string) { switch name { case "BenchmarkStdCmd": runStdCmd() return case "BenchmarkCmdGoSize": runCmdGoSize() return case "BenchmarkHelloSize": runHelloSize() return } pkg, err := build.Import(dir, ".", 0) if err != nil { log.Print(err) return } 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 { log.Printf("%v: %v", name, err) return } end := time.Now() var allocs, bytes 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": bytes = val case "Mallocs": allocs = val } } if *flagMemprofile != "" { if err := ioutil.WriteFile(*flagMemprofile, 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) } if err := ioutil.WriteFile(*flagCpuprofile, out, 0666); err != nil { log.Print(err) } os.Remove(pkg.Dir + "/_compilebench_.cpuprof") } wallns := end.Sub(start).Nanoseconds() userns := cmd.ProcessState.UserTime().Nanoseconds() if *flagAlloc { fmt.Printf("%s 1 %d ns/op %d user-ns/op %d B/op %d allocs/op\n", name, wallns, userns, bytes, allocs) } else { fmt.Printf("%s 1 %d ns/op %d user-ns/op\n", name, wallns, userns) } os.Remove(pkg.Dir + "/_compilebench_.o") }