1
0
mirror of https://github.com/golang/go synced 2024-11-19 02:04:42 -07:00
go/cmd/compilebench/main.go
Russ Cox 4432cd1c27 cmd/compilebench: fix GOROOT inconsistencies
If GOROOT is unset (as is common now),
then the output of "go env GOROOT" may
differ from the GOROOT inferred by alternate
compilers being executed and also from the
value of runtime.GOROOT() inside compilebench,
used by go/build. Harmonize all of these by
setting GOROOT explicitly (will fix subcommands)
and by invoking the go command to learn about
packages instead of assuming go/build has any
idea what it's doing.

Change-Id: If97aa76cc2afec11a8404975f39329db7eb452e0
Reviewed-on: https://go-review.googlesource.com/109516
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: David Chase <drchase@google.com>
2018-08-10 00:27:41 +00:00

379 lines
10 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.
//
// -memprofile file
// Write a memory profile of the compiler to file.
//
// -memprofilerate rate
// Set runtime.MemProfileRate during compilation.
//
// -obj
// Report object file statistics.
//
// -pkg
// Benchmark compiling a single package.
//
// -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 (
"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")
)
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()
}
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
}
for i := 0; i < *flagCount; i++ {
if *flagPackage != "" {
runBuild("BenchmarkPkg", *flagPackage, i)
continue
}
for _, tt := range tests {
if tt.long && *flagShort {
continue
}
if runRE == nil || runRE.MatchString(tt.name) {
runBuild(tt.name, tt.dir, i)
}
}
}
}
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() {
args := []string{"build", "-a"}
if *flagCompilerFlags != "" {
args = append(args, "-gcflags", *flagCompilerFlags)
}
args = append(args, "std", "cmd")
cmd := exec.Command(*flagGoCmd, args...)
cmd.Dir = filepath.Join(goroot, "src")
runCmd("BenchmarkStdCmd", cmd)
}
// path is either a path to a file ("$GOROOT/test/helloworld.go") or a package path ("cmd/go").
func runSize(name, path string) {
cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", path)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Print(err)
return
}
defer os.Remove("_compilebenchout_")
info, err := os.Stat("_compilebenchout_")
if err != nil {
log.Print(err)
return
}
out, err := exec.Command("size", "_compilebenchout_").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, count int) {
switch name {
case "BenchmarkStdCmd":
runStdCmd()
return
case "BenchmarkCmdGoSize":
runSize("BenchmarkCmdGoSize", "cmd/go")
return
case "BenchmarkHelloSize":
runSize("BenchmarkHelloSize", filepath.Join(goroot, "test/helloworld.go"))
return
}
// Make sure dependencies needed by go tool compile are installed to GOROOT/pkg.
out, err := exec.Command(*flagGoCmd, "build", "-i", dir).CombinedOutput()
if err != nil {
log.Printf("go build -i %s: %v\n%s", dir, err, out)
return
}
// Find dir and source file list.
var pkg struct {
Dir string
GoFiles []string
}
out, err = exec.Command(*flagGoCmd, "list", "-json", dir).Output()
if err != nil {
log.Printf("go list -json %s: %v\n", dir, err)
return
}
if err := json.Unmarshal(out, &pkg); err != nil {
log.Printf("go list -json %s: unmarshal: %v", dir, 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, 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 != "" {
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)
}
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)
}