1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 package main
78
79 import (
80 "bytes"
81 "encoding/json"
82 "flag"
83 "fmt"
84 "log"
85 "os"
86 "os/exec"
87 "path/filepath"
88 "regexp"
89 "runtime"
90 "strconv"
91 "strings"
92 "time"
93 )
94
95 var (
96 goroot string
97 compiler string
98 assembler string
99 linker string
100 runRE *regexp.Regexp
101 is6g bool
102 needCompilingRuntimeFlag bool
103 )
104
105 var (
106 flagGoCmd = flag.String("go", "go", "path to \"go\" command")
107 flagAlloc = flag.Bool("alloc", false, "report allocations")
108 flagObj = flag.Bool("obj", false, "report object file stats")
109 flagCompiler = flag.String("compile", "", "use `exe` as the cmd/compile binary")
110 flagAssembler = flag.String("asm", "", "use `exe` as the cmd/asm binary")
111 flagCompilerFlags = flag.String("compileflags", "", "additional `flags` to pass to compile")
112 flagLinker = flag.String("link", "", "use `exe` as the cmd/link binary")
113 flagLinkerFlags = flag.String("linkflags", "", "additional `flags` to pass to link")
114 flagRun = flag.String("run", "", "run benchmarks matching `regexp`")
115 flagCount = flag.Int("count", 1, "run benchmarks `n` times")
116 flagCpuprofile = flag.String("cpuprofile", "", "write CPU profile to `file`")
117 flagMemprofile = flag.String("memprofile", "", "write memory profile to `file`")
118 flagMemprofilerate = flag.Int64("memprofilerate", -1, "set memory profile `rate`")
119 flagPackage = flag.String("pkg", "", "if set, benchmark the package at path `pkg`")
120 flagShort = flag.Bool("short", false, "skip long-running benchmarks")
121 flagTrace = flag.Bool("trace", false, "debug tracing of builds")
122 )
123
124 type test struct {
125 name string
126 r runner
127 }
128
129 type runner interface {
130 long() bool
131 run(name string, count int) error
132 }
133
134 var tests = []test{
135 {"BenchmarkTemplate", compile{"html/template"}},
136 {"BenchmarkUnicode", compile{"unicode"}},
137 {"BenchmarkGoTypes", compile{"go/types"}},
138 {"BenchmarkCompiler", compile{"cmd/compile/internal/gc"}},
139 {"BenchmarkSSA", compile{"cmd/compile/internal/ssa"}},
140 {"BenchmarkFlate", compile{"compress/flate"}},
141 {"BenchmarkGoParser", compile{"go/parser"}},
142 {"BenchmarkReflect", compile{"reflect"}},
143 {"BenchmarkTar", compile{"archive/tar"}},
144 {"BenchmarkXML", compile{"encoding/xml"}},
145 {"BenchmarkLinkCompiler", link{"cmd/compile", ""}},
146 {"BenchmarkExternalLinkCompiler", link{"cmd/compile", "-linkmode=external"}},
147 {"BenchmarkLinkWithoutDebugCompiler", link{"cmd/compile", "-w"}},
148 {"BenchmarkStdCmd", goBuild{[]string{"std", "cmd"}}},
149 {"BenchmarkHelloSize", size{"$GOROOT/test/helloworld.go", false}},
150 {"BenchmarkCmdGoSize", size{"cmd/go", true}},
151 }
152
153 func usage() {
154 fmt.Fprintf(os.Stderr, "usage: compilebench [options]\n")
155 fmt.Fprintf(os.Stderr, "options:\n")
156 flag.PrintDefaults()
157 os.Exit(2)
158 }
159
160 func main() {
161 log.SetFlags(0)
162 log.SetPrefix("compilebench: ")
163 flag.Usage = usage
164 flag.Parse()
165 if flag.NArg() != 0 {
166 usage()
167 }
168
169 s, err := exec.Command(*flagGoCmd, "env", "GOROOT").CombinedOutput()
170 if err != nil {
171 log.Fatalf("%s env GOROOT: %v", *flagGoCmd, err)
172 }
173 goroot = strings.TrimSpace(string(s))
174 os.Setenv("GOROOT", goroot)
175
176 compiler = *flagCompiler
177 if compiler == "" {
178 var foundTool string
179 foundTool, compiler = toolPath("compile", "6g")
180 if foundTool == "6g" {
181 is6g = true
182 }
183 }
184 assembler = *flagAssembler
185 if assembler == "" {
186 _, assembler = toolPath("asm")
187 }
188 if err := checkCompilingRuntimeFlag(assembler); err != nil {
189 log.Fatalf("checkCompilingRuntimeFlag: %v", err)
190 }
191
192 linker = *flagLinker
193 if linker == "" && !is6g {
194 _, linker = toolPath("link")
195 }
196
197 if is6g {
198 *flagMemprofilerate = -1
199 *flagAlloc = false
200 *flagCpuprofile = ""
201 *flagMemprofile = ""
202 }
203
204 if *flagRun != "" {
205 r, err := regexp.Compile(*flagRun)
206 if err != nil {
207 log.Fatalf("invalid -run argument: %v", err)
208 }
209 runRE = r
210 }
211
212 if *flagPackage != "" {
213 tests = []test{
214 {"BenchmarkPkg", compile{*flagPackage}},
215 {"BenchmarkPkgLink", link{*flagPackage, ""}},
216 }
217 runRE = nil
218 }
219
220 for i := 0; i < *flagCount; i++ {
221 for _, tt := range tests {
222 if tt.r.long() && *flagShort {
223 continue
224 }
225 if runRE == nil || runRE.MatchString(tt.name) {
226 if err := tt.r.run(tt.name, i); err != nil {
227 log.Printf("%s: %v", tt.name, err)
228 }
229 }
230 }
231 }
232 }
233
234 func toolPath(names ...string) (found, path string) {
235 var out1 []byte
236 var err1 error
237 for i, name := range names {
238 out, err := exec.Command(*flagGoCmd, "tool", "-n", name).CombinedOutput()
239 if err == nil {
240 return name, strings.TrimSpace(string(out))
241 }
242 if i == 0 {
243 out1, err1 = out, err
244 }
245 }
246 log.Fatalf("go tool -n %s: %v\n%s", names[0], err1, out1)
247 return "", ""
248 }
249
250 type Pkg struct {
251 ImportPath string
252 Dir string
253 GoFiles []string
254 SFiles []string
255 }
256
257 func goList(dir string) (*Pkg, error) {
258 var pkg Pkg
259 out, err := exec.Command(*flagGoCmd, "list", "-json", dir).Output()
260 if err != nil {
261 return nil, fmt.Errorf("go list -json %s: %v", dir, err)
262 }
263 if err := json.Unmarshal(out, &pkg); err != nil {
264 return nil, fmt.Errorf("go list -json %s: unmarshal: %v", dir, err)
265 }
266 return &pkg, nil
267 }
268
269 func runCmd(name string, cmd *exec.Cmd) error {
270 start := time.Now()
271 out, err := cmd.CombinedOutput()
272 if err != nil {
273 return fmt.Errorf("%v\n%s", err, out)
274 }
275 fmt.Printf("%s 1 %d ns/op\n", name, time.Since(start).Nanoseconds())
276 return nil
277 }
278
279 type goBuild struct{ pkgs []string }
280
281 func (goBuild) long() bool { return true }
282
283 func (r goBuild) run(name string, count int) error {
284 args := []string{"build", "-a"}
285 if *flagCompilerFlags != "" {
286 args = append(args, "-gcflags", *flagCompilerFlags)
287 }
288 args = append(args, r.pkgs...)
289 cmd := exec.Command(*flagGoCmd, args...)
290 cmd.Dir = filepath.Join(goroot, "src")
291 return runCmd(name, cmd)
292 }
293
294 type size struct {
295
296 path string
297 isLong bool
298 }
299
300 func (r size) long() bool { return r.isLong }
301
302 func (r size) run(name string, count int) error {
303 if strings.HasPrefix(r.path, "$GOROOT/") {
304 r.path = goroot + "/" + r.path[len("$GOROOT/"):]
305 }
306
307 cmd := exec.Command(*flagGoCmd, "build", "-o", "_compilebenchout_", r.path)
308 cmd.Stdout = os.Stderr
309 cmd.Stderr = os.Stderr
310 if err := cmd.Run(); err != nil {
311 return err
312 }
313 defer os.Remove("_compilebenchout_")
314 info, err := os.Stat("_compilebenchout_")
315 if err != nil {
316 return err
317 }
318 out, err := exec.Command("size", "_compilebenchout_").CombinedOutput()
319 if err != nil {
320 return fmt.Errorf("size: %v\n%s", err, out)
321 }
322 lines := strings.Split(string(out), "\n")
323 if len(lines) < 2 {
324 return fmt.Errorf("not enough output from size: %s", out)
325 }
326 f := strings.Fields(lines[1])
327 if strings.HasPrefix(lines[0], "__TEXT") && len(f) >= 2 {
328 fmt.Printf("%s 1 %s text-bytes %s data-bytes %v exe-bytes\n", name, f[0], f[1], info.Size())
329 } else if strings.Contains(lines[0], "bss") && len(f) >= 3 {
330 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())
331 }
332 return nil
333 }
334
335 type compile struct{ dir string }
336
337 func (compile) long() bool { return false }
338
339 func (c compile) run(name string, count int) error {
340
341 out, err := exec.Command(*flagGoCmd, "build", c.dir).CombinedOutput()
342 if err != nil {
343 return fmt.Errorf("go build %s: %v\n%s", c.dir, err, out)
344 }
345
346
347 pkg, err := goList(c.dir)
348 if err != nil {
349 return err
350 }
351
352 importcfg, err := genImportcfgFile(c.dir, "", false)
353 if err != nil {
354 return err
355 }
356
357
358
359
360 var symAbisFile string
361 var asmIncFile string
362 if len(pkg.SFiles) != 0 {
363 symAbisFile = filepath.Join(pkg.Dir, "symabis")
364 asmIncFile = filepath.Join(pkg.Dir, "go_asm.h")
365 content := "\n"
366 if err := os.WriteFile(asmIncFile, []byte(content), 0666); err != nil {
367 return fmt.Errorf("os.WriteFile(%s) failed: %v", asmIncFile, err)
368 }
369 defer os.Remove(symAbisFile)
370 defer os.Remove(asmIncFile)
371 if err := genSymAbisFile(pkg, symAbisFile, pkg.Dir); err != nil {
372 return err
373 }
374 }
375
376 args := []string{"-o", "_compilebench_.o", "-p", pkg.ImportPath}
377 args = append(args, strings.Fields(*flagCompilerFlags)...)
378 if symAbisFile != "" {
379 args = append(args, "-symabis", symAbisFile)
380 }
381 if importcfg != "" {
382 args = append(args, "-importcfg", importcfg)
383 defer os.Remove(importcfg)
384 }
385 args = append(args, pkg.GoFiles...)
386 if err := runBuildCmd(name, count, pkg.Dir, compiler, args); err != nil {
387 return err
388 }
389
390 opath := pkg.Dir + "/_compilebench_.o"
391 if *flagObj {
392
393 data, err := os.ReadFile(opath)
394 if err != nil {
395 log.Print(err)
396 }
397
398 i := bytes.Index(data, []byte("\n$$B\n")) + len("\n$$B\n")
399
400 nexport := bytes.Index(data[i:], []byte("\n$$\n"))
401 fmt.Printf(" %d object-bytes %d export-bytes", len(data), nexport)
402 }
403 fmt.Println()
404
405 os.Remove(opath)
406 return nil
407 }
408
409 type link struct{ dir, flags string }
410
411 func (link) long() bool { return false }
412
413 func (r link) run(name string, count int) error {
414 if linker == "" {
415
416 return nil
417 }
418
419
420 ldflags := *flagLinkerFlags
421 if r.flags != "" {
422 if ldflags != "" {
423 ldflags += " "
424 }
425 ldflags += r.flags
426 }
427 out, err := exec.Command(*flagGoCmd, "build", "-o", "/dev/null", "-ldflags="+ldflags, r.dir).CombinedOutput()
428 if err != nil {
429 return fmt.Errorf("go build -a %s: %v\n%s", r.dir, err, out)
430 }
431
432 importcfg, err := genImportcfgFile(r.dir, "-ldflags="+ldflags, true)
433 if err != nil {
434 return err
435 }
436 defer os.Remove(importcfg)
437
438
439 pkg, err := goList(r.dir)
440 if err != nil {
441 return err
442 }
443 args := []string{"-o", "_compilebench_.o", "-importcfg", importcfg}
444 args = append(args, pkg.GoFiles...)
445 if *flagTrace {
446 fmt.Fprintf(os.Stderr, "running: %s %+v\n",
447 compiler, args)
448 }
449 cmd := exec.Command(compiler, args...)
450 cmd.Dir = pkg.Dir
451 cmd.Stdout = os.Stderr
452 cmd.Stderr = os.Stderr
453 err = cmd.Run()
454 if err != nil {
455 return fmt.Errorf("compiling: %v", err)
456 }
457 defer os.Remove(pkg.Dir + "/_compilebench_.o")
458
459
460 args = []string{"-o", "_compilebench_.exe", "-importcfg", importcfg}
461 args = append(args, strings.Fields(*flagLinkerFlags)...)
462 args = append(args, strings.Fields(r.flags)...)
463 args = append(args, "_compilebench_.o")
464 if err := runBuildCmd(name, count, pkg.Dir, linker, args); err != nil {
465 return err
466 }
467 fmt.Println()
468 defer os.Remove(pkg.Dir + "/_compilebench_.exe")
469
470 return err
471 }
472
473
474
475
476
477
478
479 func runBuildCmd(name string, count int, dir, tool string, args []string) error {
480 var preArgs []string
481 if *flagMemprofilerate >= 0 {
482 preArgs = append(preArgs, "-memprofilerate", fmt.Sprint(*flagMemprofilerate))
483 }
484 if *flagAlloc || *flagCpuprofile != "" || *flagMemprofile != "" {
485 if *flagAlloc || *flagMemprofile != "" {
486 preArgs = append(preArgs, "-memprofile", "_compilebench_.memprof")
487 }
488 if *flagCpuprofile != "" {
489 preArgs = append(preArgs, "-cpuprofile", "_compilebench_.cpuprof")
490 }
491 }
492 if *flagTrace {
493 fmt.Fprintf(os.Stderr, "running: %s %+v\n",
494 tool, append(preArgs, args...))
495 }
496 cmd := exec.Command(tool, append(preArgs, args...)...)
497 cmd.Dir = dir
498 cmd.Stdout = os.Stderr
499 cmd.Stderr = os.Stderr
500 start := time.Now()
501 err := cmd.Run()
502 if err != nil {
503 return err
504 }
505 end := time.Now()
506
507 haveAllocs, haveRSS := false, false
508 var allocs, allocbytes, rssbytes int64
509 if *flagAlloc || *flagMemprofile != "" {
510 out, err := os.ReadFile(dir + "/_compilebench_.memprof")
511 if err != nil {
512 log.Print("cannot find memory profile after compilation")
513 }
514 for _, line := range strings.Split(string(out), "\n") {
515 f := strings.Fields(line)
516 if len(f) < 4 || f[0] != "#" || f[2] != "=" {
517 continue
518 }
519 val, err := strconv.ParseInt(f[3], 0, 64)
520 if err != nil {
521 continue
522 }
523 haveAllocs = true
524 switch f[1] {
525 case "TotalAlloc":
526 allocbytes = val
527 case "Mallocs":
528 allocs = val
529 case "MaxRSS":
530 haveRSS = true
531 rssbytes = val
532 }
533 }
534 if !haveAllocs {
535 log.Println("missing stats in memprof (golang.org/issue/18641)")
536 }
537
538 if *flagMemprofile != "" {
539 outpath := *flagMemprofile
540 if *flagCount != 1 {
541 outpath = fmt.Sprintf("%s_%d", outpath, count)
542 }
543 if err := os.WriteFile(outpath, out, 0666); err != nil {
544 log.Print(err)
545 }
546 }
547 os.Remove(dir + "/_compilebench_.memprof")
548 }
549
550 if *flagCpuprofile != "" {
551 out, err := os.ReadFile(dir + "/_compilebench_.cpuprof")
552 if err != nil {
553 log.Print(err)
554 }
555 outpath := *flagCpuprofile
556 if *flagCount != 1 {
557 outpath = fmt.Sprintf("%s_%d", outpath, count)
558 }
559 if err := os.WriteFile(outpath, out, 0666); err != nil {
560 log.Print(err)
561 }
562 os.Remove(dir + "/_compilebench_.cpuprof")
563 }
564
565 wallns := end.Sub(start).Nanoseconds()
566 userns := cmd.ProcessState.UserTime().Nanoseconds()
567
568 fmt.Printf("%s 1 %d ns/op %d user-ns/op", name, wallns, userns)
569 if haveAllocs {
570 fmt.Printf(" %d B/op %d allocs/op", allocbytes, allocs)
571 }
572 if haveRSS {
573 fmt.Printf(" %d maxRSS/op", rssbytes)
574 }
575
576 return nil
577 }
578
579 func checkCompilingRuntimeFlag(assembler string) error {
580 td, err := os.MkdirTemp("", "asmsrcd")
581 if err != nil {
582 return fmt.Errorf("MkdirTemp failed: %v", err)
583 }
584 defer os.RemoveAll(td)
585 src := filepath.Join(td, "asm.s")
586 obj := filepath.Join(td, "asm.o")
587 const code = `
588 TEXT ·foo(SB),$0-0
589 RET
590 `
591 if err := os.WriteFile(src, []byte(code), 0644); err != nil {
592 return fmt.Errorf("writing %s failed: %v", src, err)
593 }
594
595
596
597
598
599
600 args := []string{"-o", obj, "-p", "reflect", "-compiling-runtime", src}
601 cmd := exec.Command(assembler, args...)
602 cmd.Dir = td
603 out, aerr := cmd.CombinedOutput()
604 if aerr != nil {
605 if strings.Contains(string(out), "flag provided but not defined: -compiling-runtime") {
606
607
608 return nil
609 }
610
611 return fmt.Errorf("problems invoking assembler with args %+v: error %v\n%s\n", args, aerr, out)
612 }
613
614 needCompilingRuntimeFlag = true
615 return nil
616 }
617
618
619
620
621
622
623 func genSymAbisFile(pkg *Pkg, symAbisFile, incdir string) error {
624 args := []string{"-gensymabis", "-o", symAbisFile,
625 "-p", pkg.ImportPath,
626 "-I", filepath.Join(goroot, "pkg", "include"),
627 "-I", incdir,
628 "-D", "GOOS_" + runtime.GOOS,
629 "-D", "GOARCH_" + runtime.GOARCH}
630 if pkg.ImportPath == "reflect" && needCompilingRuntimeFlag {
631 args = append(args, "-compiling-runtime")
632 }
633 args = append(args, pkg.SFiles...)
634 if *flagTrace {
635 fmt.Fprintf(os.Stderr, "running: %s %+v\n",
636 assembler, args)
637 }
638 cmd := exec.Command(assembler, args...)
639 cmd.Dir = pkg.Dir
640 cmd.Stdout = os.Stderr
641 cmd.Stderr = os.Stderr
642 err := cmd.Run()
643 if err != nil {
644 return fmt.Errorf("assembling to produce symabis file: %v", err)
645 }
646 return nil
647 }
648
649
650
651
652 func genImportcfgFile(dir string, flags string, full bool) (string, error) {
653 need := "{{.Imports}}"
654 if full {
655
656 need = "{{.Deps}}"
657 }
658
659 if flags == "" {
660 flags = "--"
661 }
662
663
664 cmd := exec.Command(*flagGoCmd, "list", "-f", need, flags, dir)
665 cmd.Stderr = os.Stderr
666 out, err := cmd.Output()
667 if err != nil {
668 return "", fmt.Errorf("go list -f %s %s: %v", need, dir, err)
669 }
670
671 if len(out) < 3 || out[0] != '[' || out[len(out)-2] != ']' || out[len(out)-1] != '\n' {
672 return "", fmt.Errorf("unexpected output from go list -f %s %s: %s", need, dir, out)
673 }
674 out = out[1 : len(out)-2]
675 if len(out) == 0 {
676 return "", nil
677 }
678
679
680 cmd = exec.Command(*flagGoCmd, "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}", flags)
681 cmd.Args = append(cmd.Args, strings.Fields(string(out))...)
682 cmd.Stderr = os.Stderr
683 out, err = cmd.Output()
684 if err != nil {
685 return "", fmt.Errorf("generating importcfg for %s: %s: %v", dir, cmd, err)
686 }
687
688 f, err := os.CreateTemp("", "importcfg")
689 if err != nil {
690 return "", fmt.Errorf("creating tmp importcfg file failed: %v", err)
691 }
692 defer f.Close()
693 if _, err := f.Write(out); err != nil {
694 return "", fmt.Errorf("writing importcfg file %s failed: %v", f.Name(), err)
695 }
696 return f.Name(), nil
697 }
698
View as plain text