diff --git a/compilebench/main.go b/compilebench/main.go new file mode 100644 index 00000000..9050ac13 --- /dev/null +++ b/compilebench/main.go @@ -0,0 +1,319 @@ +// 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}, + {"BenchmarkMakeBash", "", 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 runMakeBash() { + cmd := exec.Command("./make.bash") + cmd.Dir = filepath.Join(runtime.GOROOT(), "src") + runCmd("BenchmarkMakeBash", 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 "BenchmarkMakeBash": + runMakeBash() + 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") +}