github.com/april1989/origin-go-tools@v0.0.32/cmd/compilebench/main.go (about)

     1  // Copyright 2015 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Compilebench benchmarks the speed of the Go compiler.
     6  //
     7  // Usage:
     8  //
     9  //	compilebench [options]
    10  //
    11  // It times the compilation of various packages and prints results in
    12  // the format used by package golibexec_testing (and expected by golang.org/x/perf/cmd/benchstat).
    13  //
    14  // The options are:
    15  //
    16  //	-alloc
    17  //		Report allocations.
    18  //
    19  //	-compile exe
    20  //		Use exe as the path to the cmd/compile binary.
    21  //
    22  //	-compileflags 'list'
    23  //		Pass the space-separated list of flags to the compilation.
    24  //
    25  //	-link exe
    26  //		Use exe as the path to the cmd/link binary.
    27  //
    28  //	-linkflags 'list'
    29  //		Pass the space-separated list of flags to the linker.
    30  //
    31  //	-count n
    32  //		Run each benchmark n times (default 1).
    33  //
    34  //	-cpuprofile file
    35  //		Write a CPU profile of the compiler to file.
    36  //
    37  //	-go path
    38  //		Path to "go" command (default "go").
    39  //
    40  //	-memprofile file
    41  //		Write a memory profile of the compiler to file.
    42  //
    43  //	-memprofilerate rate
    44  //		Set runtime.MemProfileRate during compilation.
    45  //
    46  //	-obj
    47  //		Report object file statistics.
    48  //
    49  //	-pkg pkg
    50  //		Benchmark compiling a single package.
    51  //
    52  //	-run regexp
    53  //		Only run benchmarks with names matching regexp.
    54  //
    55  //	-short
    56  //		Skip long-running benchmarks.
    57  //
    58  // Although -cpuprofile and -memprofile are intended to write a
    59  // combined profile for all the executed benchmarks to file,
    60  // today they write only the profile for the last benchmark executed.
    61  //
    62  // The default memory profiling rate is one profile sample per 512 kB
    63  // allocated (see ``go doc runtime.MemProfileRate'').
    64  // Lowering the rate (for example, -memprofilerate 64000) produces
    65  // a more fine-grained and therefore accurate profile, but it also incurs
    66  // execution cost. For benchmark comparisons, never use timings
    67  // obtained with a low -memprofilerate option.
    68  //
    69  // Example
    70  //
    71  // Assuming the base version of the compiler has been saved with
    72  // ``toolstash save,'' this sequence compares the old and new compiler:
    73  //
    74  //	compilebench -count 10 -compile $(toolstash -n compile) >old.txt
    75  //	compilebench -count 10 >new.txt
    76  //	benchstat old.txt new.txt
    77  //
    78  package main
    79  
    80  import (
    81  	"bytes"
    82  	"encoding/json"
    83  	"flag"
    84  	"fmt"
    85  	"io/ioutil"
    86  	"log"
    87  	"os"
    88  	"os/exec"
    89  	"path/filepath"
    90  	"regexp"
    91  	"strconv"
    92  	"strings"
    93  	"time"
    94  )
    95  
    96  var (
    97  	goroot   string
    98  	compiler string
    99  	linker   string
   100  	runRE    *regexp.Regexp
   101  	is6g     bool
   102  )
   103  
   104  var (
   105  	flagGoCmd          = flag.String("go", "go", "path to \"go\" command")
   106  	flagAlloc          = flag.Bool("alloc", false, "report allocations")
   107  	flagObj            = flag.Bool("obj", false, "report object file stats")
   108  	flagCompiler       = flag.String("compile", "", "use `exe` as the cmd/compile binary")
   109  	flagCompilerFlags  = flag.String("compileflags", "", "additional `flags` to pass to compile")
   110  	flagLinker         = flag.String("link", "", "use `exe` as the cmd/link binary")
   111  	flagLinkerFlags    = flag.String("linkflags", "", "additional `flags` to pass to link")
   112  	flagRun            = flag.String("run", "", "run benchmarks matching `regexp`")
   113  	flagCount          = flag.Int("count", 1, "run benchmarks `n` times")
   114  	flagCpuprofile     = flag.String("cpuprofile", "", "write CPU profile to `file`")
   115  	flagMemprofile     = flag.String("memprofile", "", "write memory profile to `file`")
   116  	flagMemprofilerate = flag.Int64("memprofilerate", -1, "set memory profile `rate`")
   117  	flagPackage        = flag.String("pkg", "", "if set, benchmark the package at path `pkg`")
   118  	flagShort          = flag.Bool("short", false, "skip long-running benchmarks")
   119  )
   120  
   121  type test struct {
   122  	name string
   123  	r    runner
   124  }
   125  
   126  type runner interface {
   127  	long() bool
   128  	run(name string, count int) error
   129  }
   130  
   131  var tests = []test{
   132  	{"BenchmarkTemplate", compile{"html/template"}},
   133  	{"BenchmarkUnicode", compile{"unicode"}},
   134  	{"BenchmarkGoTypes", compile{"go/types"}},
   135  	{"BenchmarkCompiler", compile{"cmd/compile/internal/gc"}},
   136  	{"BenchmarkSSA", compile{"cmd/compile/internal/ssa"}},
   137  	{"BenchmarkFlate", compile{"compress/flate"}},
   138  	{"BenchmarkGoParser", compile{"go/parser"}},
   139  	{"BenchmarkReflect", compile{"reflect"}},
   140  	{"BenchmarkTar", compile{"archive/tar"}},
   141  	{"BenchmarkXML", compile{"encoding/xml"}},
   142  	{"BenchmarkLinkCompiler", link{"cmd/compile", ""}},
   143  	{"BenchmarkExternalLinkCompiler", link{"cmd/compile", "-linkmode=external"}},
   144  	{"BenchmarkLinkWithoutDebugCompiler", link{"cmd/compile", "-w"}},
   145  	{"BenchmarkStdCmd", goBuild{[]string{"std", "cmd"}}},
   146  	{"BenchmarkHelloSize", size{"$GOROOT/test/helloworld.go", false}},
   147  	{"BenchmarkCmdGoSize", size{"cmd/go", true}},
   148  }
   149  
   150  func usage() {
   151  	fmt.Fprintf(os.Stderr, "usage: compilebench [options]\n")
   152  	fmt.Fprintf(os.Stderr, "options:\n")
   153  	flag.PrintDefaults()
   154  	os.Exit(2)
   155  }
   156  
   157  func main() {
   158  	log.SetFlags(0)
   159  	log.SetPrefix("compilebench: ")
   160  	flag.Usage = usage
   161  	flag.Parse()
   162  	if flag.NArg() != 0 {
   163  		usage()
   164  	}
   165  
   166  	s, err := exec.Command(*flagGoCmd, "env", "GOROOT").CombinedOutput()
   167  	if err != nil {
   168  		log.Fatalf("%s env GOROOT: %v", *flagGoCmd, err)
   169  	}
   170  	goroot = strings.TrimSpace(string(s))
   171  	os.Setenv("GOROOT", goroot) // for any subcommands
   172  
   173  	compiler = *flagCompiler
   174  	if compiler == "" {
   175  		var foundTool string
   176  		foundTool, compiler = toolPath("compile", "6g")
   177  		if foundTool == "6g" {
   178  			is6g = true
   179  		}
   180  	}
   181  
   182  	linker = *flagLinker
   183  	if linker == "" && !is6g { // TODO: Support 6l
   184  		_, linker = toolPath("link")
   185  	}
   186  
   187  	if is6g {
   188  		*flagMemprofilerate = -1
   189  		*flagAlloc = false
   190  		*flagCpuprofile = ""
   191  		*flagMemprofile = ""
   192  	}
   193  
   194  	if *flagRun != "" {
   195  		r, err := regexp.Compile(*flagRun)
   196  		if err != nil {
   197  			log.Fatalf("invalid -run argument: %v", err)
   198  		}
   199  		runRE = r
   200  	}
   201  
   202  	if *flagPackage != "" {
   203  		tests = []test{
   204  			{"BenchmarkPkg", compile{*flagPackage}},
   205  			{"BenchmarkPkgLink", link{*flagPackage, ""}},
   206  		}
   207  		runRE = nil
   208  	}
   209  
   210  	for i := 0; i < *flagCount; i++ {
   211  		for _, tt := range tests {
   212  			if tt.r.long() && *flagShort {
   213  				continue
   214  			}
   215  			if runRE == nil || runRE.MatchString(tt.name) {
   216  				if err := tt.r.run(tt.name, i); err != nil {
   217  					log.Printf("%s: %v", tt.name, err)
   218  				}
   219  			}
   220  		}
   221  	}
   222  }
   223  
   224  func toolPath(names ...string) (found, path string) {
   225  	var out1 []byte
   226  	var err1 error
   227  	for i, name := range names {
   228  		out, err := exec.Command(*flagGoCmd, "tool", "-n", name).CombinedOutput()
   229  		if err == nil {
   230  			return name, strings.TrimSpace(string(out))
   231  		}
   232  		if i == 0 {
   233  			out1, err1 = out, err
   234  		}
   235  	}
   236  	log.Fatalf("go tool -n %s: %v\n%s", names[0], err1, out1)
   237  	return "", ""
   238  }
   239  
   240  type Pkg struct {
   241  	Dir     string
   242  	GoFiles []string
   243  }
   244  
   245  func goList(dir string) (*Pkg, error) {
   246  	var pkg Pkg
   247  	out, err := exec.Command(*flagGoCmd, "list", "-json", dir).Output()
   248  	if err != nil {
   249  		return nil, fmt.Errorf("go list -json %s: %v", dir, err)
   250  	}
   251  	if err := json.Unmarshal(out, &pkg); err != nil {
   252  		return nil, fmt.Errorf("go list -json %s: unmarshal: %v", dir, err)
   253  	}
   254  	return &pkg, nil
   255  }
   256  
   257  func runCmd(name string, cmd *exec.Cmd) error {
   258  	start := time.Now()
   259  	out, err := cmd.CombinedOutput()
   260  	if err != nil {
   261  		return fmt.Errorf("%v\n%s", err, out)
   262  	}
   263  	fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds())
   264  	return nil
   265  }
   266  
   267  type goBuild struct{ pkgs []string }
   268  
   269  func (goBuild) long() bool { return true }
   270  
   271  func (r goBuild) run(name string, count int) error {
   272  	args := []string{"build", "-a"}
   273  	if *flagCompilerFlags != "" {
   274  		args = append(args, "-gcflags", *flagCompilerFlags)
   275  	}
   276  	args = append(args, r.pkgs...)
   277  	cmd := exec.Command(*flagGoCmd, args...)
   278  	cmd.Dir = filepath.Join(goroot, "src")
   279  	return runCmd(name, cmd)
   280  }
   281  
   282  type size struct {
   283  	// path is either a path to a file ("$GOROOT/test/helloworld.go") or a package path ("cmd/go").
   284  	path   string
   285  	isLong bool
   286  }
   287  
   288  func (r size) long() bool { return r.isLong }
   289  
   290  func (r size) run(name string, count int) error {
   291  	if strings.HasPrefix(r.path, "$GOROOT/") {
   292  		r.path = goroot + "/" + r.path[len("$GOROOT/"):]
   293  	}
   294  
   295  	cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", r.path)
   296  	cmd.Stdout = os.Stderr
   297  	cmd.Stderr = os.Stderr
   298  	if err := cmd.Run(); err != nil {
   299  		return err
   300  	}
   301  	defer os.Remove("_compilebenchout_")
   302  	info, err := os.Stat("_compilebenchout_")
   303  	if err != nil {
   304  		return err
   305  	}
   306  	out, err := exec.Command("size", "_compilebenchout_").CombinedOutput()
   307  	if err != nil {
   308  		return fmt.Errorf("size: %v\n%s", err, out)
   309  	}
   310  	lines := strings.Split(string(out), "\n")
   311  	if len(lines) < 2 {
   312  		return fmt.Errorf("not enough output from size: %s", out)
   313  	}
   314  	f := strings.Fields(lines[1])
   315  	if strings.HasPrefix(lines[0], "__TEXT") && len(f) >= 2 { // OS X
   316  		fmt.Printf("%s 1 %s text-bytes %s data-bytes %v exe-bytes\n", name, f[0], f[1], info.Size())
   317  	} else if strings.Contains(lines[0], "bss") && len(f) >= 3 {
   318  		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())
   319  	}
   320  	return nil
   321  }
   322  
   323  type compile struct{ dir string }
   324  
   325  func (compile) long() bool { return false }
   326  
   327  func (c compile) run(name string, count int) error {
   328  	// Make sure dependencies needed by go tool compile are installed to GOROOT/pkg.
   329  	out, err := exec.Command(*flagGoCmd, "build", "-i", c.dir).CombinedOutput()
   330  	if err != nil {
   331  		return fmt.Errorf("go build -i %s: %v\n%s", c.dir, err, out)
   332  	}
   333  
   334  	// Find dir and source file list.
   335  	pkg, err := goList(c.dir)
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	args := []string{"-o", "_compilebench_.o"}
   341  	args = append(args, strings.Fields(*flagCompilerFlags)...)
   342  	args = append(args, pkg.GoFiles...)
   343  	if err := runBuildCmd(name, count, pkg.Dir, compiler, args); err != nil {
   344  		return err
   345  	}
   346  
   347  	opath := pkg.Dir + "/_compilebench_.o"
   348  	if *flagObj {
   349  		// TODO(josharian): object files are big; just read enough to find what we seek.
   350  		data, err := ioutil.ReadFile(opath)
   351  		if err != nil {
   352  			log.Print(err)
   353  		}
   354  		// Find start of export data.
   355  		i := bytes.Index(data, []byte("\n$$B\n")) + len("\n$$B\n")
   356  		// Count bytes to end of export data.
   357  		nexport := bytes.Index(data[i:], []byte("\n$$\n"))
   358  		fmt.Printf(" %d object-bytes %d export-bytes", len(data), nexport)
   359  	}
   360  	fmt.Println()
   361  
   362  	os.Remove(opath)
   363  	return nil
   364  }
   365  
   366  type link struct{ dir, flags string }
   367  
   368  func (link) long() bool { return false }
   369  
   370  func (r link) run(name string, count int) error {
   371  	if linker == "" {
   372  		// No linker. Skip the test.
   373  		return nil
   374  	}
   375  
   376  	// Build dependencies.
   377  	out, err := exec.Command(*flagGoCmd, "build", "-i", "-o", "/dev/null", r.dir).CombinedOutput()
   378  	if err != nil {
   379  		return fmt.Errorf("go build -i %s: %v\n%s", r.dir, err, out)
   380  	}
   381  
   382  	// Build the main package.
   383  	pkg, err := goList(r.dir)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	args := []string{"-o", "_compilebench_.o"}
   388  	args = append(args, pkg.GoFiles...)
   389  	cmd := exec.Command(compiler, args...)
   390  	cmd.Dir = pkg.Dir
   391  	cmd.Stdout = os.Stderr
   392  	cmd.Stderr = os.Stderr
   393  	err = cmd.Run()
   394  	if err != nil {
   395  		return fmt.Errorf("compiling: %v", err)
   396  	}
   397  	defer os.Remove(pkg.Dir + "/_compilebench_.o")
   398  
   399  	// Link the main package.
   400  	args = []string{"-o", "_compilebench_.exe"}
   401  	args = append(args, strings.Fields(*flagLinkerFlags)...)
   402  	args = append(args, strings.Fields(r.flags)...)
   403  	args = append(args, "_compilebench_.o")
   404  	if err := runBuildCmd(name, count, pkg.Dir, linker, args); err != nil {
   405  		return err
   406  	}
   407  	fmt.Println()
   408  	defer os.Remove(pkg.Dir + "/_compilebench_.exe")
   409  
   410  	return err
   411  }
   412  
   413  // runBuildCmd runs "tool args..." in dir, measures standard build
   414  // tool metrics, and prints a benchmark line. The caller may print
   415  // additional metrics and then must print a newline.
   416  //
   417  // This assumes tool accepts standard build tool flags like
   418  // -memprofilerate, -memprofile, and -cpuprofile.
   419  func runBuildCmd(name string, count int, dir, tool string, args []string) error {
   420  	var preArgs []string
   421  	if *flagMemprofilerate >= 0 {
   422  		preArgs = append(preArgs, "-memprofilerate", fmt.Sprint(*flagMemprofilerate))
   423  	}
   424  	if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" {
   425  		if *flagAlloc || *flagMemprofile != "" {
   426  			preArgs = append(preArgs, "-memprofile", "_compilebench_.memprof")
   427  		}
   428  		if *flagCpuprofile != "" {
   429  			preArgs = append(preArgs, "-cpuprofile", "_compilebench_.cpuprof")
   430  		}
   431  	}
   432  	cmd := exec.Command(tool, append(preArgs, args...)...)
   433  	cmd.Dir = dir
   434  	cmd.Stdout = os.Stderr
   435  	cmd.Stderr = os.Stderr
   436  	start := time.Now()
   437  	err := cmd.Run()
   438  	if err != nil {
   439  		return err
   440  	}
   441  	end := time.Now()
   442  
   443  	haveAllocs, haveRSS := false, false
   444  	var allocs, allocbytes, rssbytes int64
   445  	if *flagAlloc || *flagMemprofile != "" {
   446  		out, err := ioutil.ReadFile(dir + "/_compilebench_.memprof")
   447  		if err != nil {
   448  			log.Print("cannot find memory profile after compilation")
   449  		}
   450  		for _, line := range strings.Split(string(out), "\n") {
   451  			f := strings.Fields(line)
   452  			if len(f) < 4 || f[0] != "#" || f[2] != "=" {
   453  				continue
   454  			}
   455  			val, err := strconv.ParseInt(f[3], 0, 64)
   456  			if err != nil {
   457  				continue
   458  			}
   459  			haveAllocs = true
   460  			switch f[1] {
   461  			case "TotalAlloc":
   462  				allocbytes = val
   463  			case "Mallocs":
   464  				allocs = val
   465  			case "MaxRSS":
   466  				haveRSS = true
   467  				rssbytes = val
   468  			}
   469  		}
   470  		if !haveAllocs {
   471  			log.Println("missing stats in memprof (golang.org/issue/18641)")
   472  		}
   473  
   474  		if *flagMemprofile != "" {
   475  			outpath := *flagMemprofile
   476  			if *flagCount != 1 {
   477  				outpath = fmt.Sprintf("%s_%d", outpath, count)
   478  			}
   479  			if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
   480  				log.Print(err)
   481  			}
   482  		}
   483  		os.Remove(dir + "/_compilebench_.memprof")
   484  	}
   485  
   486  	if *flagCpuprofile != "" {
   487  		out, err := ioutil.ReadFile(dir + "/_compilebench_.cpuprof")
   488  		if err != nil {
   489  			log.Print(err)
   490  		}
   491  		outpath := *flagCpuprofile
   492  		if *flagCount != 1 {
   493  			outpath = fmt.Sprintf("%s_%d", outpath, count)
   494  		}
   495  		if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
   496  			log.Print(err)
   497  		}
   498  		os.Remove(dir + "/_compilebench_.cpuprof")
   499  	}
   500  
   501  	wallns := end.Sub(start).Nanoseconds()
   502  	userns := cmd.ProcessState.UserTime().Nanoseconds()
   503  
   504  	fmt.Printf("%s 1 %d ns/op %d user-ns/op", name, wallns, userns)
   505  	if haveAllocs {
   506  		fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs)
   507  	}
   508  	if haveRSS {
   509  		fmt.Printf(" %d maxRSS/op", rssbytes)
   510  	}
   511  
   512  	return nil
   513  }