Skip to content

Commit ab2829e

Browse files
committed
cmd/compile: adjust start heap size
TLDR - not-huge increase to default starting heap boost, - small improvement in build performance, - remove concurrency dependence of starting heap, - aligns RSS behavior with GOMEMLIMIT, - adds a gcflags=-d=gcstart=N (N -> N MiB) flag for people who want to trade a lot of memory for a little build performance improvement. This removes concurrency (-c flag) sensitivity and increases the nominal default to 128MiB. Refactored the startheap code into a separate file, to make it easier to extract and reuse. Added sensitivity to concurrency=1 and GOMEMLIMIT!="" (in addition to existing GOGC!=""), those disable the default starting heap boost because the compiler-invoker has indicated either a desire to control the GC or a desire to run in minimum memory(or both). Adds a -d flag gcstart=N (N is number of MiB) for tinkering/experiments. This always enables the starting heap. (`GOGC=XXX` and `-d=gcstart=YYY` will use `GOGC=XXX` after starting heap size is achieved.) Derated the "boost" obtained by a factor of .70 so that `-d=gcstart=2000` yields the same RSS as `GOMEMLIMIT=2000MiB` (Actually adjusts the boost with a high-low breakpoint.) The parent, with concurrency sensitivity, provided 64MB of plain boost. Derating reduces the effects of boosting the starting heap slightly. The benchmark here shows that maintaining 64MB results in a minor regression, while increasing it to 128MB produces a slight improvement, and does not grow the RSS versus 64MB. ``` │ parent │ sh64 │ sh128 │ sh1024 │ │ sec/op │ sec/op vs base │ sec/op vs base │ sec/op vs base │ std 10.164 ± 1% 10.527 ± 1% +3.57% (p=0.000 n=50) 10.084 ± 1% -0.79% (p=0.000 n=50) 9.631 ± 1% -5.24% (p=0.000 n=50) compile 21.05 ± 1% 20.78 ± 0% -1.28% (p=0.000 n=50) 20.74 ± 1% -1.46% (p=0.000 n=50) 20.77 ± 0% -1.32% (p=0.001 n=50) ast 20.45 ± 1% 20.39 ± 1% ~ (p=0.334 n=50) 20.44 ± 0% ~ (p=0.818 n=50) 20.11 ± 1% -1.65% (p=0.000 n=50) geomean 16.35 16.46 +0.65% 16.23 -0.76% 15.90 -2.75% │ parent │ sh64 │ sh128 │ sh1024 │ │ user-sec/op │ user-sec/op vs base │ user-sec/op vs base │ user-sec/op vs base │ std 66.06 ± 0% 69.74 ± 0% +5.56% (p=0.000 n=50) 64.68 ± 0% -2.09% (p=0.000 n=50) 59.51 ± 0% -9.91% (p=0.000 n=50) compile 84.69 ± 1% 82.54 ± 0% -2.53% (p=0.000 n=50) 82.63 ± 0% -2.43% (p=0.000 n=50) 82.66 ± 1% -2.40% (p=0.000 n=50) ast 59.41 ± 0% 58.84 ± 1% -0.95% (p=0.011 n=50) 59.48 ± 1% ~ (p=0.341 n=50) 57.13 ± 1% -3.83% (p=0.000 n=50) geomean 69.27 69.71 +0.63% 68.25 -1.47% 65.50 -5.44% │ parent │ sh64 │ sh128 │ sh1024 │ │ sys-sec/op │ sys-sec/op vs base │ sys-sec/op vs base │ sys-sec/op vs base │ std 9.599 ± 1% 10.031 ± 1% +4.50% (p=0.000 n=50) 9.513 ± 1% -0.90% (p=0.014 n=50) 9.359 ± 1% -2.50% (p=0.000 n=50) compile 6.813 ± 1% 6.740 ± 1% -1.08% (p=0.017 n=50) 6.716 ± 1% -1.42% (p=0.006 n=50) 6.696 ± 1% -1.72% (p=0.000 n=50) ast 4.315 ± 1% 4.291 ± 1% ~ (p=0.781 n=50) 4.296 ± 1% ~ (p=0.792 n=50) 4.279 ± 2% ~ (p=0.124 n=50) geomean 6.559 6.620 +0.93% 6.499 -0.92% 6.449 -1.68% │ parent │ sh64 │ sh128 │ sh1024 │ │ peak-RSS-bytes │ peak-RSS-bytes vs base │ peak-RSS-bytes vs base │ peak-RSS-bytes vs base │ std 257.1Mi ± 1% 257.2Mi ± 1% ~ (p=0.754 n=50) 257.0Mi ± 0% ~ (p=0.570 n=50) 605.6Mi ± 0% +135.59% (p=0.000 n=50) compile 1007.2Mi ± 1% 1004.3Mi ± 0% ~ (p=0.064 n=50) 1007.4Mi ± 0% ~ (p=0.348 n=50) 1009.4Mi ± 1% ~ (p=0.598 n=50) ast 1.848Gi ± 0% 1.842Gi ± 0% ~ (p=0.079 n=50) 1.824Gi ± 0% -1.25% (p=0.000 n=50) 1.856Gi ± 0% +0.47% (p=0.000 n=50) geomean 788.3Mi 786.8Mi -0.19% 785.0Mi -0.41% 1.027Gi +33.37% ``` Updates #73044 Change-Id: I6359642a94b396e696dd57e64ed1f2c4cf178475 Reviewed-on: https://go-review.googlesource.com/c/go/+/724441 Reviewed-by: Keith Randall <khr@google.com> Reviewed-by: Keith Randall <khr@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 54b82e9 commit ab2829e

File tree

4 files changed

+280
-198
lines changed

4 files changed

+280
-198
lines changed

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

Lines changed: 0 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
package base
66

77
import (
8-
"fmt"
98
"os"
10-
"runtime"
11-
"runtime/debug"
12-
"runtime/metrics"
139
)
1410

1511
var atExitFuncs []func()
@@ -29,193 +25,3 @@ func Exit(code int) {
2925

3026
// To enable tracing support (-t flag), set EnableTrace to true.
3127
const EnableTrace = false
32-
33-
// forEachGC calls fn each GC cycle until it returns false.
34-
func forEachGC(fn func() bool) {
35-
type T [32]byte // large enough to avoid runtime's tiny object allocator
36-
37-
var finalizer func(*T)
38-
finalizer = func(p *T) {
39-
if fn() {
40-
runtime.SetFinalizer(p, finalizer)
41-
}
42-
}
43-
44-
finalizer(new(T))
45-
}
46-
47-
// AdjustStartingHeap modifies GOGC so that GC should not occur until the heap
48-
// grows to the requested size. This is intended but not promised, though it
49-
// is true-mostly, depending on when the adjustment occurs and on the
50-
// compiler's input and behavior. Once this size is approximately reached
51-
// GOGC is reset to 100; subsequent GCs may reduce the heap below the requested
52-
// size, but this function does not affect that.
53-
//
54-
// -d=gcadjust=1 enables logging of GOGC adjustment events.
55-
//
56-
// NOTE: If you think this code would help startup time in your own
57-
// application and you decide to use it, please benchmark first to see if it
58-
// actually works for you (it may not: the Go compiler is not typical), and
59-
// whatever the outcome, please leave a comment on bug #56546. This code
60-
// uses supported interfaces, but depends more than we like on
61-
// current+observed behavior of the garbage collector, so if many people need
62-
// this feature, we should consider/propose a better way to accomplish it.
63-
func AdjustStartingHeap(requestedHeapGoal uint64) {
64-
logHeapTweaks := Debug.GCAdjust == 1
65-
mp := runtime.GOMAXPROCS(0)
66-
gcConcurrency := Flag.LowerC
67-
68-
const (
69-
goal = "/gc/heap/goal:bytes"
70-
count = "/gc/cycles/total:gc-cycles"
71-
allocs = "/gc/heap/allocs:bytes"
72-
frees = "/gc/heap/frees:bytes"
73-
)
74-
75-
sample := []metrics.Sample{{Name: goal}, {Name: count}, {Name: allocs}, {Name: frees}}
76-
const (
77-
GOAL = 0
78-
COUNT = 1
79-
ALLOCS = 2
80-
FREES = 3
81-
)
82-
83-
// Assumptions and observations of Go's garbage collector, as of Go 1.17-1.20:
84-
85-
// - the initial heap goal is 4M, by fiat. It is possible for Go to start
86-
// with a heap as small as 512k, so this may change in the future.
87-
88-
// - except for the first heap goal, heap goal is a function of
89-
// observed-live at the previous GC and current GOGC. After the first
90-
// GC, adjusting GOGC immediately updates GOGC; before the first GC,
91-
// adjusting GOGC does not modify goal (but the change takes effect after
92-
// the first GC).
93-
94-
// - the before/after first GC behavior is not guaranteed anywhere, it's
95-
// just behavior, and it's a bad idea to rely on it.
96-
97-
// - we don't know exactly when GC will run, even after we adjust GOGC; the
98-
// first GC may not have happened yet, may have already happened, or may
99-
// be currently in progress, and GCs can start for several reasons.
100-
101-
// - forEachGC above will run the provided function at some delay after each
102-
// GC's mark phase terminates; finalizers are run after marking as the
103-
// spans containing finalizable objects are swept, driven by GC
104-
// background activity and allocation demand.
105-
106-
// - "live at last GC" is not available through the current metrics
107-
// interface. Instead, live is estimated by knowing the adjusted value of
108-
// GOGC and the new heap goal following a GC (this requires knowing that
109-
// at least one GC has occurred):
110-
// estLive = 100 * newGoal / (100 + currentGogc)
111-
// this new value of GOGC
112-
// newGogc = 100*requestedHeapGoal/estLive - 100
113-
// will result in the desired goal. The logging code checks that the
114-
// resulting goal is correct.
115-
116-
// There's a small risk that the finalizer will be slow to run after a GC
117-
// that expands the goal to a huge value, and that this will lead to
118-
// out-of-memory. This doesn't seem to happen; in experiments on a variety
119-
// of machines with a variety of extra loads to disrupt scheduling, the
120-
// worst overshoot observed was 50% past requestedHeapGoal.
121-
122-
metrics.Read(sample)
123-
for _, s := range sample {
124-
if s.Value.Kind() == metrics.KindBad {
125-
// Just return, a slightly slower compilation is a tolerable outcome.
126-
if logHeapTweaks {
127-
fmt.Fprintf(os.Stderr, "GCAdjust: Regret unexpected KindBad for metric %s\n", s.Name)
128-
}
129-
return
130-
}
131-
}
132-
133-
// Tinker with GOGC to make the heap grow rapidly at first.
134-
currentGoal := sample[GOAL].Value.Uint64() // Believe this will be 4MByte or less, perhaps 512k
135-
myGogc := 100 * requestedHeapGoal / currentGoal
136-
if myGogc <= 150 {
137-
return
138-
}
139-
140-
if logHeapTweaks {
141-
sample := append([]metrics.Sample(nil), sample...) // avoid races with GC callback
142-
AtExit(func() {
143-
metrics.Read(sample)
144-
goal := sample[GOAL].Value.Uint64()
145-
count := sample[COUNT].Value.Uint64()
146-
oldGogc := debug.SetGCPercent(100)
147-
if oldGogc == 100 {
148-
fmt.Fprintf(os.Stderr, "GCAdjust: AtExit goal %d gogc %d count %d maxprocs %d gcConcurrency %d\n",
149-
goal, oldGogc, count, mp, gcConcurrency)
150-
} else {
151-
inUse := sample[ALLOCS].Value.Uint64() - sample[FREES].Value.Uint64()
152-
overPct := 100 * (int(inUse) - int(requestedHeapGoal)) / int(requestedHeapGoal)
153-
fmt.Fprintf(os.Stderr, "GCAdjust: AtExit goal %d gogc %d count %d maxprocs %d gcConcurrency %d overPct %d\n",
154-
goal, oldGogc, count, mp, gcConcurrency, overPct)
155-
156-
}
157-
})
158-
}
159-
160-
debug.SetGCPercent(int(myGogc))
161-
162-
adjustFunc := func() bool {
163-
164-
metrics.Read(sample)
165-
goal := sample[GOAL].Value.Uint64()
166-
count := sample[COUNT].Value.Uint64()
167-
168-
if goal <= requestedHeapGoal { // Stay the course
169-
if logHeapTweaks {
170-
fmt.Fprintf(os.Stderr, "GCAdjust: Reuse GOGC adjust, current goal %d, count is %d, current gogc %d\n",
171-
goal, count, myGogc)
172-
}
173-
return true
174-
}
175-
176-
// Believe goal has been adjusted upwards, else it would be less-than-or-equal than requestedHeapGoal
177-
calcLive := 100 * goal / (100 + myGogc)
178-
179-
if 2*calcLive < requestedHeapGoal { // calcLive can exceed requestedHeapGoal!
180-
myGogc = 100*requestedHeapGoal/calcLive - 100
181-
182-
if myGogc > 125 {
183-
// Not done growing the heap.
184-
oldGogc := debug.SetGCPercent(int(myGogc))
185-
186-
if logHeapTweaks {
187-
// Check that the new goal looks right
188-
inUse := sample[ALLOCS].Value.Uint64() - sample[FREES].Value.Uint64()
189-
metrics.Read(sample)
190-
newGoal := sample[GOAL].Value.Uint64()
191-
pctOff := 100 * (int64(newGoal) - int64(requestedHeapGoal)) / int64(requestedHeapGoal)
192-
// Check that the new goal is close to requested. 3% of make.bash fails this test. Why, TBD.
193-
if pctOff < 2 {
194-
fmt.Fprintf(os.Stderr, "GCAdjust: Retry GOGC adjust, current goal %d, count is %d, gogc was %d, is now %d, calcLive %d pctOff %d\n",
195-
goal, count, oldGogc, myGogc, calcLive, pctOff)
196-
} else {
197-
// The GC is being annoying and not giving us the goal that we requested, say more to help understand when/why.
198-
fmt.Fprintf(os.Stderr, "GCAdjust: Retry GOGC adjust, current goal %d, count is %d, gogc was %d, is now %d, calcLive %d pctOff %d inUse %d\n",
199-
goal, count, oldGogc, myGogc, calcLive, pctOff, inUse)
200-
}
201-
}
202-
return true
203-
}
204-
}
205-
206-
// In this case we're done boosting GOGC, set it to 100 and don't set a new finalizer.
207-
oldGogc := debug.SetGCPercent(100)
208-
// inUse helps estimate how late the finalizer ran; at the instant the previous GC ended,
209-
// it was (in theory) equal to the previous GC's heap goal. In a growing heap it is
210-
// expected to grow to the new heap goal.
211-
inUse := sample[ALLOCS].Value.Uint64() - sample[FREES].Value.Uint64()
212-
overPct := 100 * (int(inUse) - int(requestedHeapGoal)) / int(requestedHeapGoal)
213-
if logHeapTweaks {
214-
fmt.Fprintf(os.Stderr, "GCAdjust: Reset GOGC adjust, old goal %d, count is %d, gogc was %d, calcLive %d inUse %d overPct %d\n",
215-
goal, count, oldGogc, calcLive, inUse, overPct)
216-
}
217-
return false
218-
}
219-
220-
forEachGC(adjustFunc)
221-
}

src/cmd/compile/internal/base/debug.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type DebugFlags struct {
3838
GCAdjust int `help:"log adjustments to GOGC" concurrent:"ok"`
3939
GCCheck int `help:"check heap/gc use by compiler" concurrent:"ok"`
4040
GCProg int `help:"print dump of GC programs"`
41+
GCStart int `help:"specify \"starting\" compiler's heap size in MiB" concurrent:"ok"`
4142
Gossahash string `help:"hash value for use in debugging the compiler"`
4243
InlFuncsWithClosures int `help:"allow functions with closures to be inlined" concurrent:"ok"`
4344
InlStaticInit int `help:"allow static initialization of inlined calls" concurrent:"ok"`

0 commit comments

Comments
 (0)