summaryrefslogtreecommitdiffstats
path: root/src/cmd/compile/internal/base/base.go
blob: ee3772c5ca2bfa44feae2978405fd61b4ae4ebd9 (plain)
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package base

import (
	"fmt"
	"os"
	"runtime"
	"runtime/debug"
	"runtime/metrics"
)

var atExitFuncs []func()

func AtExit(f func()) {
	atExitFuncs = append(atExitFuncs, f)
}

func Exit(code int) {
	for i := len(atExitFuncs) - 1; i >= 0; i-- {
		f := atExitFuncs[i]
		atExitFuncs = atExitFuncs[:i]
		f()
	}
	os.Exit(code)
}

// To enable tracing support (-t flag), set EnableTrace to true.
const EnableTrace = false

// forEachGC calls fn each GC cycle until it returns false.
func forEachGC(fn func() bool) {
	type T [32]byte // large enough to avoid runtime's tiny object allocator

	var finalizer func(*T)
	finalizer = func(p *T) {
		if fn() {
			runtime.SetFinalizer(p, finalizer)
		}
	}

	finalizer(new(T))
}

// AdjustStartingHeap modifies GOGC so that GC should not occur until the heap
// grows to the requested size.  This is intended but not promised, though it
// is true-mostly, depending on when the adjustment occurs and on the
// compiler's input and behavior.  Once this size is approximately reached
// GOGC is reset to 100; subsequent GCs may reduce the heap below the requested
// size, but this function does not affect that.
//
// -d=gcadjust=1 enables logging of GOGC adjustment events.
//
// NOTE: If you think this code would help startup time in your own
// application and you decide to use it, please benchmark first to see if it
// actually works for you (it may not: the Go compiler is not typical), and
// whatever the outcome, please leave a comment on bug #56546.  This code
// uses supported interfaces, but depends more than we like on
// current+observed behavior of the garbage collector, so if many people need
// this feature, we should consider/propose a better way to accomplish it.
func AdjustStartingHeap(requestedHeapGoal uint64) {
	logHeapTweaks := Debug.GCAdjust == 1
	mp := runtime.GOMAXPROCS(0)
	gcConcurrency := Flag.LowerC

	const (
		goal   = "/gc/heap/goal:bytes"
		count  = "/gc/cycles/total:gc-cycles"
		allocs = "/gc/heap/allocs:bytes"
		frees  = "/gc/heap/frees:bytes"
	)

	sample := []metrics.Sample{{Name: goal}, {Name: count}, {Name: allocs}, {Name: frees}}
	const (
		GOAL   = 0
		COUNT  = 1
		ALLOCS = 2
		FREES  = 3
	)

	// Assumptions and observations of Go's garbage collector, as of Go 1.17-1.20:

	// - the initial heap goal is 4M, by fiat.  It is possible for Go to start
	//   with a heap as small as 512k, so this may change in the future.

	// - except for the first heap goal, heap goal is a function of
	//   observed-live at the previous GC and current GOGC.  After the first
	//   GC, adjusting GOGC immediately updates GOGC; before the first GC,
	//   adjusting GOGC does not modify goal (but the change takes effect after
	//   the first GC).

	// - the before/after first GC behavior is not guaranteed anywhere, it's
	//   just behavior, and it's a bad idea to rely on it.

	// - we don't know exactly when GC will run, even after we adjust GOGC; the
	//   first GC may not have happened yet, may have already happened, or may
	//   be currently in progress, and GCs can start for several reasons.

	// - forEachGC above will run the provided function at some delay after each
	//   GC's mark phase terminates; finalizers are run after marking as the
	//   spans containing finalizable objects are swept, driven by GC
	//   background activity and allocation demand.

	// - "live at last GC" is not available through the current metrics
	//    interface. Instead, live is estimated by knowing the adjusted value of
	//    GOGC and the new heap goal following a GC (this requires knowing that
	//    at least one GC has occurred):
	//		  estLive = 100 * newGoal / (100 + currentGogc)
	//    this new value of GOGC
	//		  newGogc = 100*requestedHeapGoal/estLive - 100
	//    will result in the desired goal. The logging code checks that the
	//    resulting goal is correct.

	// There's a small risk that the finalizer will be slow to run after a GC
	// that expands the goal to a huge value, and that this will lead to
	// out-of-memory.  This doesn't seem to happen; in experiments on a variety
	// of machines with a variety of extra loads to disrupt scheduling, the
	// worst overshoot observed was 50% past requestedHeapGoal.

	metrics.Read(sample)
	for _, s := range sample {
		if s.Value.Kind() == metrics.KindBad {
			// Just return, a slightly slower compilation is a tolerable outcome.
			if logHeapTweaks {
				fmt.Fprintf(os.Stderr, "GCAdjust: Regret unexpected KindBad for metric %s\n", s.Name)
			}
			return
		}
	}

	// Tinker with GOGC to make the heap grow rapidly at first.
	currentGoal := sample[GOAL].Value.Uint64() // Believe this will be 4MByte or less, perhaps 512k
	myGogc := 100 * requestedHeapGoal / currentGoal
	if myGogc <= 150 {
		return
	}

	if logHeapTweaks {
		sample := append([]metrics.Sample(nil), sample...) // avoid races with GC callback
		AtExit(func() {
			metrics.Read(sample)
			goal := sample[GOAL].Value.Uint64()
			count := sample[COUNT].Value.Uint64()
			oldGogc := debug.SetGCPercent(100)
			if oldGogc == 100 {
				fmt.Fprintf(os.Stderr, "GCAdjust: AtExit goal %d gogc %d count %d maxprocs %d gcConcurrency %d\n",
					goal, oldGogc, count, mp, gcConcurrency)
			} else {
				inUse := sample[ALLOCS].Value.Uint64() - sample[FREES].Value.Uint64()
				overPct := 100 * (int(inUse) - int(requestedHeapGoal)) / int(requestedHeapGoal)
				fmt.Fprintf(os.Stderr, "GCAdjust: AtExit goal %d gogc %d count %d maxprocs %d gcConcurrency %d overPct %d\n",
					goal, oldGogc, count, mp, gcConcurrency, overPct)

			}
		})
	}

	debug.SetGCPercent(int(myGogc))

	adjustFunc := func() bool {

		metrics.Read(sample)
		goal := sample[GOAL].Value.Uint64()
		count := sample[COUNT].Value.Uint64()

		if goal <= requestedHeapGoal { // Stay the course
			if logHeapTweaks {
				fmt.Fprintf(os.Stderr, "GCAdjust: Reuse GOGC adjust, current goal %d, count is %d, current gogc %d\n",
					goal, count, myGogc)
			}
			return true
		}

		// Believe goal has been adjusted upwards, else it would be less-than-or-equal than requestedHeapGoal
		calcLive := 100 * goal / (100 + myGogc)

		if 2*calcLive < requestedHeapGoal { // calcLive can exceed requestedHeapGoal!
			myGogc = 100*requestedHeapGoal/calcLive - 100

			if myGogc > 125 {
				// Not done growing the heap.
				oldGogc := debug.SetGCPercent(int(myGogc))

				if logHeapTweaks {
					// Check that the new goal looks right
					inUse := sample[ALLOCS].Value.Uint64() - sample[FREES].Value.Uint64()
					metrics.Read(sample)
					newGoal := sample[GOAL].Value.Uint64()
					pctOff := 100 * (int64(newGoal) - int64(requestedHeapGoal)) / int64(requestedHeapGoal)
					// Check that the new goal is close to requested.  3% of make.bash fails this test.  Why, TBD.
					if pctOff < 2 {
						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",
							goal, count, oldGogc, myGogc, calcLive, pctOff)
					} else {
						// The GC is being annoying and not giving us the goal that we requested, say more to help understand when/why.
						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",
							goal, count, oldGogc, myGogc, calcLive, pctOff, inUse)
					}
				}
				return true
			}
		}

		// In this case we're done boosting GOGC, set it to 100 and don't set a new finalizer.
		oldGogc := debug.SetGCPercent(100)
		// inUse helps estimate how late the finalizer ran; at the instant the previous GC ended,
		// it was (in theory) equal to the previous GC's heap goal.  In a growing heap it is
		// expected to grow to the new heap goal.
		inUse := sample[ALLOCS].Value.Uint64() - sample[FREES].Value.Uint64()
		overPct := 100 * (int(inUse) - int(requestedHeapGoal)) / int(requestedHeapGoal)
		if logHeapTweaks {
			fmt.Fprintf(os.Stderr, "GCAdjust: Reset GOGC adjust, old goal %d, count is %d, gogc was %d, calcLive %d inUse %d overPct %d\n",
				goal, count, oldGogc, calcLive, inUse, overPct)
		}
		return false
	}

	forEachGC(adjustFunc)
}