You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

360 lines
9.7 KiB

  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. // Compilebench benchmarks the speed of the Go compiler.
  5. //
  6. // Usage:
  7. //
  8. // compilebench [options]
  9. //
  10. // It times the compilation of various packages and prints results in
  11. // the format used by package testing (and expected by golang.org/x/perf/cmd/benchstat).
  12. //
  13. // The options are:
  14. //
  15. // -alloc
  16. // Report allocations.
  17. //
  18. // -compile exe
  19. // Use exe as the path to the cmd/compile binary.
  20. //
  21. // -compileflags 'list'
  22. // Pass the space-separated list of flags to the compilation.
  23. //
  24. // -count n
  25. // Run each benchmark n times (default 1).
  26. //
  27. // -cpuprofile file
  28. // Write a CPU profile of the compiler to file.
  29. //
  30. // -memprofile file
  31. // Write a memory profile of the compiler to file.
  32. //
  33. // -memprofilerate rate
  34. // Set runtime.MemProfileRate during compilation.
  35. //
  36. // -obj
  37. // Report object file statistics.
  38. //
  39. // -pkg
  40. // Benchmark compiling a single package.
  41. //
  42. // -run regexp
  43. // Only run benchmarks with names matching regexp.
  44. //
  45. // Although -cpuprofile and -memprofile are intended to write a
  46. // combined profile for all the executed benchmarks to file,
  47. // today they write only the profile for the last benchmark executed.
  48. //
  49. // The default memory profiling rate is one profile sample per 512 kB
  50. // allocated (see ``go doc runtime.MemProfileRate'').
  51. // Lowering the rate (for example, -memprofilerate 64000) produces
  52. // a more fine-grained and therefore accurate profile, but it also incurs
  53. // execution cost. For benchmark comparisons, never use timings
  54. // obtained with a low -memprofilerate option.
  55. //
  56. // Example
  57. //
  58. // Assuming the base version of the compiler has been saved with
  59. // ``toolstash save,'' this sequence compares the old and new compiler:
  60. //
  61. // compilebench -count 10 -compile $(toolstash -n compile) >old.txt
  62. // compilebench -count 10 >new.txt
  63. // benchstat old.txt new.txt
  64. //
  65. package main
  66. import (
  67. "bytes"
  68. "flag"
  69. "fmt"
  70. "go/build"
  71. "io/ioutil"
  72. "log"
  73. "os"
  74. "os/exec"
  75. "path/filepath"
  76. "regexp"
  77. "strconv"
  78. "strings"
  79. "time"
  80. )
  81. var (
  82. goroot string
  83. compiler string
  84. runRE *regexp.Regexp
  85. is6g bool
  86. )
  87. var (
  88. flagGoCmd = flag.String("go", "go", "path to \"go\" command")
  89. flagAlloc = flag.Bool("alloc", false, "report allocations")
  90. flagObj = flag.Bool("obj", false, "report object file stats")
  91. flagCompiler = flag.String("compile", "", "use `exe` as the cmd/compile binary")
  92. flagCompilerFlags = flag.String("compileflags", "", "additional `flags` to pass to compile")
  93. flagRun = flag.String("run", "", "run benchmarks matching `regexp`")
  94. flagCount = flag.Int("count", 1, "run benchmarks `n` times")
  95. flagCpuprofile = flag.String("cpuprofile", "", "write CPU profile to `file`")
  96. flagMemprofile = flag.String("memprofile", "", "write memory profile to `file`")
  97. flagMemprofilerate = flag.Int64("memprofilerate", -1, "set memory profile `rate`")
  98. flagPackage = flag.String("pkg", "", "if set, benchmark the package at path `pkg`")
  99. flagShort = flag.Bool("short", false, "skip long-running benchmarks")
  100. )
  101. var tests = []struct {
  102. name string
  103. dir string
  104. long bool
  105. }{
  106. {"BenchmarkTemplate", "html/template", false},
  107. {"BenchmarkUnicode", "unicode", false},
  108. {"BenchmarkGoTypes", "go/types", false},
  109. {"BenchmarkCompiler", "cmd/compile/internal/gc", false},
  110. {"BenchmarkSSA", "cmd/compile/internal/ssa", false},
  111. {"BenchmarkFlate", "compress/flate", false},
  112. {"BenchmarkGoParser", "go/parser", false},
  113. {"BenchmarkReflect", "reflect", false},
  114. {"BenchmarkTar", "archive/tar", false},
  115. {"BenchmarkXML", "encoding/xml", false},
  116. {"BenchmarkStdCmd", "", true},
  117. {"BenchmarkHelloSize", "", false},
  118. {"BenchmarkCmdGoSize", "", true},
  119. }
  120. func usage() {
  121. fmt.Fprintf(os.Stderr, "usage: compilebench [options]\n")
  122. fmt.Fprintf(os.Stderr, "options:\n")
  123. flag.PrintDefaults()
  124. os.Exit(2)
  125. }
  126. func main() {
  127. log.SetFlags(0)
  128. log.SetPrefix("compilebench: ")
  129. flag.Usage = usage
  130. flag.Parse()
  131. if flag.NArg() != 0 {
  132. usage()
  133. }
  134. s, err := exec.Command(*flagGoCmd, "env", "GOROOT").CombinedOutput()
  135. if err != nil {
  136. log.Fatalf("%s env GOROOT: %v", *flagGoCmd, err)
  137. }
  138. goroot = strings.TrimSpace(string(s))
  139. compiler = *flagCompiler
  140. if compiler == "" {
  141. out, err := exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
  142. if err != nil {
  143. out, err = exec.Command(*flagGoCmd, "tool", "-n", "6g").CombinedOutput()
  144. is6g = true
  145. if err != nil {
  146. out, err = exec.Command(*flagGoCmd, "tool", "-n", "compile").CombinedOutput()
  147. log.Fatalf("go tool -n compiler: %v\n%s", err, out)
  148. }
  149. }
  150. compiler = strings.TrimSpace(string(out))
  151. }
  152. if *flagRun != "" {
  153. r, err := regexp.Compile(*flagRun)
  154. if err != nil {
  155. log.Fatalf("invalid -run argument: %v", err)
  156. }
  157. runRE = r
  158. }
  159. for i := 0; i < *flagCount; i++ {
  160. if *flagPackage != "" {
  161. runBuild("BenchmarkPkg", *flagPackage, i)
  162. continue
  163. }
  164. for _, tt := range tests {
  165. if tt.long && *flagShort {
  166. continue
  167. }
  168. if runRE == nil || runRE.MatchString(tt.name) {
  169. runBuild(tt.name, tt.dir, i)
  170. }
  171. }
  172. }
  173. }
  174. func runCmd(name string, cmd *exec.Cmd) {
  175. start := time.Now()
  176. out, err := cmd.CombinedOutput()
  177. if err != nil {
  178. log.Printf("%v: %v\n%s", name, err, out)
  179. return
  180. }
  181. fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds())
  182. }
  183. func runStdCmd() {
  184. args := []string{"build", "-a"}
  185. if *flagCompilerFlags != "" {
  186. args = append(args, "-gcflags", *flagCompilerFlags)
  187. }
  188. args = append(args, "std", "cmd")
  189. cmd := exec.Command(*flagGoCmd, args...)
  190. cmd.Dir = filepath.Join(goroot, "src")
  191. runCmd("BenchmarkStdCmd", cmd)
  192. }
  193. // path is either a path to a file ("$GOROOT/test/helloworld.go") or a package path ("cmd/go").
  194. func runSize(name, path string) {
  195. cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", path)
  196. cmd.Stdout = os.Stderr
  197. cmd.Stderr = os.Stderr
  198. if err := cmd.Run(); err != nil {
  199. log.Print(err)
  200. return
  201. }
  202. defer os.Remove("_compilebenchout_")
  203. info, err := os.Stat("_compilebenchout_")
  204. if err != nil {
  205. log.Print(err)
  206. return
  207. }
  208. out, err := exec.Command("size", "_compilebenchout_").CombinedOutput()
  209. if err != nil {
  210. log.Printf("size: %v\n%s", err, out)
  211. return
  212. }
  213. lines := strings.Split(string(out), "\n")
  214. if len(lines) < 2 {
  215. log.Printf("not enough output from size: %s", out)
  216. return
  217. }
  218. f := strings.Fields(lines[1])
  219. if strings.HasPrefix(lines[0], "__TEXT") && len(f) >= 2 { // OS X
  220. fmt.Printf("%s 1 %s text-bytes %s data-bytes %v exe-bytes\n", name, f[0], f[1], info.Size())
  221. } else if strings.Contains(lines[0], "bss") && len(f) >= 3 {
  222. 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())
  223. }
  224. }
  225. func runBuild(name, dir string, count int) {
  226. switch name {
  227. case "BenchmarkStdCmd":
  228. runStdCmd()
  229. return
  230. case "BenchmarkCmdGoSize":
  231. runSize("BenchmarkCmdGoSize", "cmd/go")
  232. return
  233. case "BenchmarkHelloSize":
  234. runSize("BenchmarkHelloSize", filepath.Join(goroot, "test/helloworld.go"))
  235. return
  236. }
  237. pkg, err := build.Import(dir, ".", 0)
  238. if err != nil {
  239. log.Print(err)
  240. return
  241. }
  242. args := []string{"-o", "_compilebench_.o"}
  243. if is6g {
  244. *flagMemprofilerate = -1
  245. *flagAlloc = false
  246. *flagCpuprofile = ""
  247. *flagMemprofile = ""
  248. }
  249. if *flagMemprofilerate >= 0 {
  250. args = append(args, "-memprofilerate", fmt.Sprint(*flagMemprofilerate))
  251. }
  252. args = append(args, strings.Fields(*flagCompilerFlags)...)
  253. if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" {
  254. if *flagAlloc || *flagMemprofile != "" {
  255. args = append(args, "-memprofile", "_compilebench_.memprof")
  256. }
  257. if *flagCpuprofile != "" {
  258. args = append(args, "-cpuprofile", "_compilebench_.cpuprof")
  259. }
  260. }
  261. args = append(args, pkg.GoFiles...)
  262. cmd := exec.Command(compiler, args...)
  263. cmd.Dir = pkg.Dir
  264. cmd.Stdout = os.Stderr
  265. cmd.Stderr = os.Stderr
  266. start := time.Now()
  267. err = cmd.Run()
  268. if err != nil {
  269. log.Printf("%v: %v", name, err)
  270. return
  271. }
  272. end := time.Now()
  273. var allocs, allocbytes int64
  274. if *flagAlloc || *flagMemprofile != "" {
  275. out, err := ioutil.ReadFile(pkg.Dir + "/_compilebench_.memprof")
  276. if err != nil {
  277. log.Print("cannot find memory profile after compilation")
  278. }
  279. for _, line := range strings.Split(string(out), "\n") {
  280. f := strings.Fields(line)
  281. if len(f) < 4 || f[0] != "#" || f[2] != "=" {
  282. continue
  283. }
  284. val, err := strconv.ParseInt(f[3], 0, 64)
  285. if err != nil {
  286. continue
  287. }
  288. switch f[1] {
  289. case "TotalAlloc":
  290. allocbytes = val
  291. case "Mallocs":
  292. allocs = val
  293. }
  294. }
  295. if *flagMemprofile != "" {
  296. if err := ioutil.WriteFile(*flagMemprofile, out, 0666); err != nil {
  297. log.Print(err)
  298. }
  299. }
  300. os.Remove(pkg.Dir + "/_compilebench_.memprof")
  301. }
  302. if *flagCpuprofile != "" {
  303. out, err := ioutil.ReadFile(pkg.Dir + "/_compilebench_.cpuprof")
  304. if err != nil {
  305. log.Print(err)
  306. }
  307. outpath := *flagCpuprofile
  308. if *flagCount != 1 {
  309. outpath = fmt.Sprintf("%s_%d", outpath, count)
  310. }
  311. if err := ioutil.WriteFile(outpath, out, 0666); err != nil {
  312. log.Print(err)
  313. }
  314. os.Remove(pkg.Dir + "/_compilebench_.cpuprof")
  315. }
  316. wallns := end.Sub(start).Nanoseconds()
  317. userns := cmd.ProcessState.UserTime().Nanoseconds()
  318. fmt.Printf("%s 1 %d ns/op %d user-ns/op", name, wallns, userns)
  319. if *flagAlloc {
  320. fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs)
  321. }
  322. opath := pkg.Dir + "/_compilebench_.o"
  323. if *flagObj {
  324. // TODO(josharian): object files are big; just read enough to find what we seek.
  325. data, err := ioutil.ReadFile(opath)
  326. if err != nil {
  327. log.Print(err)
  328. }
  329. // Find start of export data.
  330. i := bytes.Index(data, []byte("\n$$B\n")) + len("\n$$B\n")
  331. // Count bytes to end of export data.
  332. nexport := bytes.Index(data[i:], []byte("\n$$\n"))
  333. fmt.Printf(" %d object-bytes %d export-bytes", len(data), nexport)
  334. }
  335. fmt.Println()
  336. os.Remove(opath)
  337. }