Skip to content

Commit 92b2b88

Browse files
committed
cmd/go: avoid flag.FlagSet.VisitAll at init time
We want to error early if GOFLAGS contains any flag that isn't known to any cmd/go command. Thus, at init time we would recursively use VisitAll on each of the flagsets to populate a map of all registered flags. This was unfortunate, as populating said map constituted a whole 5% of the run-time of 'go env GOARCH'. This is because VisitAll is pretty expensive; it copies all the maps from the flagset's map to a slice, sorts the slice, then does one callback per flag. First, this was a bit wasteful. We only ever needed to query the knownFlag map if GOFLAGS wasn't empty. If it's empty, there's no work to do, thus we can skip the map populating work. Second and most important, we don't actually need the map at all. A flag.FlagSet already has a Lookup method, so we can simply recursively call those methods for each flag in GOFLAGS. Add a hasFlag func to make that evident. This mechanism is different; its upfront cost is none, but it will likely mean a handful of map lookups for each flag in GOFLAGS. However, that tradeoff is worth it; we don't expect GOFLAGS to contain thousands of flags. The most likely scenario is less than a dozen flags, in which case constructing a "unified" map is not at all a net win. One possible reason the previous mechanism was that way could be AddKnownFlag. Thankfully, the one and only use of that API was removed last year when Bryan cleaned up flag parsing in cmd/go. The wins for the existing benchmark with an empty GOFLAGS are significant: name old time/op new time/op delta ExecGoEnv-8 575µs ± 1% 549µs ± 2% -4.44% (p=0.000 n=7+8) name old sys-time/op new sys-time/op delta ExecGoEnv-8 1.69ms ± 1% 1.68ms ± 2% ~ (p=0.281 n=7+8) name old user-time/op new user-time/op delta ExecGoEnv-8 1.80ms ± 1% 1.66ms ± 2% -8.09% (p=0.000 n=7+8) To prove that a relatively large number of GOFLAGS isn't getting noticeably slower, we measured that as well, via benchcmd and GOFLAGS containing 50 valid flags: GOFLAGS=$(yes -- -race | sed 50q) benchcmd -n 500 GoEnvGOFLAGS go env GOARCH And the result, while noisy, shows no noticeable difference (note that it measures 3ms instead of 0.6ms since it's sequential): name old time/op new time/op delta GoEnvGOFLAGS 3.04ms ±32% 3.03ms ±35% ~ (p=0.156 n=487+481) Finally, we've improved the existing Go benchmark. Now it's parallel, and it also reports sys-time and user-time, which are useful metrics. Change-Id: I9b4551415cedf2f819eb184a02324b8bd919e2bd Reviewed-on: https://go-review.googlesource.com/c/go/+/248757 Reviewed-by: Bryan C. Mills <[email protected]> Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 806f478 commit 92b2b88

File tree

3 files changed

+38
-39
lines changed

3 files changed

+38
-39
lines changed

src/cmd/go/init_test.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package main_test
77
import (
88
"internal/testenv"
99
"os/exec"
10+
"sync/atomic"
1011
"testing"
1112
)
1213

@@ -15,20 +16,27 @@ import (
1516
// the benchmark if any changes were done.
1617
func BenchmarkExecGoEnv(b *testing.B) {
1718
testenv.MustHaveExec(b)
18-
b.StopTimer()
1919
gotool, err := testenv.GoTool()
2020
if err != nil {
2121
b.Fatal(err)
2222
}
23-
for i := 0; i < b.N; i++ {
24-
cmd := exec.Command(gotool, "env", "GOARCH")
2523

26-
b.StartTimer()
27-
err := cmd.Run()
28-
b.StopTimer()
24+
// We collect extra metrics.
25+
var n, userTime, systemTime int64
2926

30-
if err != nil {
31-
b.Fatal(err)
27+
b.ResetTimer()
28+
b.RunParallel(func(pb *testing.PB) {
29+
for pb.Next() {
30+
cmd := exec.Command(gotool, "env", "GOARCH")
31+
32+
if err := cmd.Run(); err != nil {
33+
b.Fatal(err)
34+
}
35+
atomic.AddInt64(&n, 1)
36+
atomic.AddInt64(&userTime, int64(cmd.ProcessState.UserTime()))
37+
atomic.AddInt64(&systemTime, int64(cmd.ProcessState.SystemTime()))
3238
}
33-
}
39+
})
40+
b.ReportMetric(float64(userTime)/float64(n), "user-ns/op")
41+
b.ReportMetric(float64(systemTime)/float64(n), "sys-ns/op")
3442
}

src/cmd/go/internal/base/base.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ var Go = &Command{
5656
// Commands initialized in package main
5757
}
5858

59+
// hasFlag reports whether a command or any of its subcommands contain the given
60+
// flag.
61+
func hasFlag(c *Command, name string) bool {
62+
if f := c.Flag.Lookup(name); f != nil {
63+
return true
64+
}
65+
for _, sub := range c.Commands {
66+
if hasFlag(sub, name) {
67+
return true
68+
}
69+
}
70+
return false
71+
}
72+
5973
// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument,
6074
func (c *Command) LongName() string {
6175
name := c.UsageLine

src/cmd/go/internal/base/goflags.go

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,7 @@ import (
1313
"cmd/go/internal/cfg"
1414
)
1515

16-
var (
17-
goflags []string // cached $GOFLAGS list; can be -x or --x form
18-
knownFlag = make(map[string]bool) // flags allowed to appear in $GOFLAGS; no leading dashes
19-
)
20-
21-
// AddKnownFlag adds name to the list of known flags for use in $GOFLAGS.
22-
func AddKnownFlag(name string) {
23-
knownFlag[name] = true
24-
}
16+
var goflags []string // cached $GOFLAGS list; can be -x or --x form
2517

2618
// GOFLAGS returns the flags from $GOFLAGS.
2719
// The list can be assumed to contain one string per flag,
@@ -38,34 +30,19 @@ func InitGOFLAGS() {
3830
return
3931
}
4032

41-
// Build list of all flags for all commands.
42-
// If no command has that flag, then we report the problem.
43-
// This catches typos while still letting users record flags in GOFLAGS
44-
// that only apply to a subset of go commands.
45-
// Commands using CustomFlags can report their flag names
46-
// by calling AddKnownFlag instead.
47-
var walkFlags func(*Command)
48-
walkFlags = func(cmd *Command) {
49-
for _, sub := range cmd.Commands {
50-
walkFlags(sub)
51-
}
52-
cmd.Flag.VisitAll(func(f *flag.Flag) {
53-
knownFlag[f.Name] = true
54-
})
33+
goflags = strings.Fields(cfg.Getenv("GOFLAGS"))
34+
if len(goflags) == 0 {
35+
// nothing to do; avoid work on later InitGOFLAGS call
36+
goflags = []string{}
37+
return
5538
}
56-
walkFlags(Go)
5739

5840
// Ignore bad flag in go env and go bug, because
5941
// they are what people reach for when debugging
6042
// a problem, and maybe they're debugging GOFLAGS.
6143
// (Both will show the GOFLAGS setting if let succeed.)
6244
hideErrors := cfg.CmdName == "env" || cfg.CmdName == "bug"
6345

64-
goflags = strings.Fields(cfg.Getenv("GOFLAGS"))
65-
if goflags == nil {
66-
goflags = []string{} // avoid work on later InitGOFLAGS call
67-
}
68-
6946
// Each of the words returned by strings.Fields must be its own flag.
7047
// To set flag arguments use -x=value instead of -x value.
7148
// For boolean flags, -x is fine instead of -x=true.
@@ -85,7 +62,7 @@ func InitGOFLAGS() {
8562
if i := strings.Index(name, "="); i >= 0 {
8663
name = name[:i]
8764
}
88-
if !knownFlag[name] {
65+
if !hasFlag(Go, name) {
8966
if hideErrors {
9067
continue
9168
}

0 commit comments

Comments
 (0)