Skip to content

Commit

Permalink
runtime: check the heap goal and trigger dynamically
Browse files Browse the repository at this point in the history
As it stands, the heap goal and the trigger are set once by
gcController.commit, and then read out of gcController. However with the
coming memory limit we need the GC to be able to respond to changes in
non-heap memory. The simplest way of achieving this is to compute the
heap goal and its associated trigger dynamically.

In order to make this easier to implement, the GC trigger is now based
on the heap goal, as opposed to the status quo of computing both
simultaneously. In many cases we just want the heap goal anyway, not
both, but we definitely need the goal to compute the trigger, because
the trigger's bounds are entirely based on the goal (the initial runway
is not). A consequence of this is that we can't rely on the trigger to
enforce a minimum heap size anymore, and we need to lift that up
directly to the goal. Specifically, we need to lift up any part of the
calculation that *could* put the trigger ahead of the goal. Luckily this
is just the heap minimum and minimum sweep distance. In the first case,
the pacer may behave slightly differently, as the heap minimum is no
longer the minimum trigger, but the actual minimum heap goal. In the
second case it should be the same, as we ensure the additional runway
for sweeping is added to both the goal *and* the trigger, as before, by
computing that in gcControllerState.commit.

There's also another place we update the heap goal: if a GC starts and
we triggered beyond the goal, we always ensure there's some runway.
That calculation uses the current trigger, which violates the rule of
keeping the goal based on the trigger. Notice, however, that using the
precomputed trigger for this isn't even quite correct: due to a bug, or
something else, we might trigger a GC beyond the precomputed trigger.

So this change also adds a "triggered" field to gcControllerState that
tracks the point at which a GC actually triggered. This is independent
of the precomputed trigger, so it's fine for the heap goal calculation
to rely on it. It also turns out, there's more than just that one place
where we really should be using the actual trigger point, so this change
fixes those up too.

Also, because the heap minimum is set by the goal and not the trigger,
the maximum trigger calculation now happens *after* the goal is set, so
the maximum trigger actually does what I originally intended (and what
the comment says): at small heaps, the pacer picks 95% of the runway as
the maximum trigger. Currently, the pacer picks a small trigger based
on a not-yet-rounded-up heap goal, so the trigger gets rounded up to the
goal, and as per the "ensure there's some runway" check, the runway ends
up at always being 64 KiB. That check is supposed to be for exceptional
circumstances, not the status quo. There's a test introduced in the last
CL that needs to be updated to accomodate this slight change in
behavior.

So, this all sounds like a lot that changed, but what we're talking about
here are really, really tight corner cases that arise from situations
outside of our control, like pathologically bad behavior on the part of
an OS or CPU. Even in these corner cases, it's very unlikely that users
will notice any difference at all. What's more important, I think, is
that the pacer behaves more closely to what all the comments describe,
and what the original intent was.

Another note: at first, one might think that computing the heap goal and
trigger dynamically introduces some raciness, but not in this CL: the heap
goal and trigger are completely static.

Allocation outside of a GC cycle may now be a bit slower than before, as
the GC trigger check is now significantly more complex. However, note
that this executes basically just as often as gcController.revise, and
that makes up for a vanishingly small part of any CPU profile. The next
CL cleans up the floating point multiplications on this path
nonetheless, just to be safe.

For #48409.

Change-Id: I280f5ad607a86756d33fb8449ad08555cbee93f9
Reviewed-on: https://go-review.googlesource.com/c/go/+/397014
Run-TryBot: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
  • Loading branch information
mknyszek committed May 3, 2022
1 parent 473c996 commit 129dcb7
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 131 deletions.
13 changes: 7 additions & 6 deletions src/runtime/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,10 +1281,11 @@ func NewGCController(gcPercent int) *GCController {
}

func (c *GCController) StartCycle(stackSize, globalsSize uint64, scannableFrac float64, gomaxprocs int) {
trigger, _ := c.trigger()
c.scannableStackSize = stackSize
c.globalsScan = globalsSize
c.heapLive = c.trigger
c.heapScan += uint64(float64(c.trigger-c.heapMarked) * scannableFrac)
c.heapLive = trigger
c.heapScan += uint64(float64(trigger-c.heapMarked) * scannableFrac)
c.startCycle(0, gomaxprocs, gcTrigger{kind: gcTriggerHeap})
}

Expand All @@ -1293,7 +1294,7 @@ func (c *GCController) AssistWorkPerByte() float64 {
}

func (c *GCController) HeapGoal() uint64 {
return c.heapGoal
return c.heapGoal()
}

func (c *GCController) HeapLive() uint64 {
Expand All @@ -1304,8 +1305,8 @@ func (c *GCController) HeapMarked() uint64 {
return c.heapMarked
}

func (c *GCController) Trigger() uint64 {
return c.trigger
func (c *GCController) Triggered() uint64 {
return c.triggered
}

type GCControllerReviseDelta struct {
Expand All @@ -1329,7 +1330,7 @@ func (c *GCController) EndCycle(bytesMarked uint64, assistTime, elapsed int64, g
c.assistTime.Store(assistTime)
c.endCycle(elapsed, gomaxprocs, false)
c.resetLive(bytesMarked)
c.commit()
c.commit(false)
}

func (c *GCController) AddIdleMarkWorker() bool {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func (a *sysStatsAggregate) compute() {
a.buckHashSys = memstats.buckhash_sys.load()
a.gcMiscSys = memstats.gcMiscSys.load()
a.otherSys = memstats.other_sys.load()
a.heapGoal = atomic.Load64(&gcController.heapGoal)
a.heapGoal = gcController.heapGoal()
a.gcCyclesDone = uint64(memstats.numgc)
a.gcCyclesForced = uint64(memstats.numforcedgc)

Expand Down
17 changes: 8 additions & 9 deletions src/runtime/mgc.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
// Next GC is after we've allocated an extra amount of memory proportional to
// the amount already in use. The proportion is controlled by GOGC environment variable
// (100 by default). If GOGC=100 and we're using 4M, we'll GC again when we get to 8M
// (this mark is tracked in gcController.heapGoal variable). This keeps the GC cost in
// (this mark is computed by the gcController.heapGoal method). This keeps the GC cost in
// linear proportion to the allocation cost. Adjusting GOGC just changes the linear constant
// (and also the amount of extra memory used).

Expand Down Expand Up @@ -401,7 +401,7 @@ var work struct {
pauseStart int64 // nanotime() of last STW

// debug.gctrace heap sizes for this cycle.
heap0, heap1, heap2, heapGoal uint64
heap0, heap1, heap2 uint64
}

// GC runs a garbage collection and blocks the caller until the
Expand Down Expand Up @@ -553,7 +553,8 @@ func (t gcTrigger) test() bool {
// we are going to trigger on this, this thread just
// atomically wrote gcController.heapLive anyway and we'll see our
// own write.
return gcController.heapLive >= gcController.trigger
trigger, _ := gcController.trigger()
return atomic.Load64(&gcController.heapLive) >= trigger
case gcTriggerTime:
if gcController.gcPercent.Load() < 0 {
return false
Expand Down Expand Up @@ -674,7 +675,6 @@ func gcStart(trigger gcTrigger) {
// Assists and workers can start the moment we start
// the world.
gcController.startCycle(now, int(gomaxprocs), trigger)
work.heapGoal = gcController.heapGoal

// Notify the CPU limiter that assists may begin.
gcCPULimiter.startGCTransition(true, 0, now)
Expand Down Expand Up @@ -985,10 +985,9 @@ func gcMarkTermination() {
// Record heapInUse for scavenger.
memstats.lastHeapInUse = gcController.heapInUse.load()

// Update GC trigger and pacing for the next cycle.
gcController.commit()
gcPaceSweeper(gcController.trigger)
gcPaceScavenger(gcController.heapGoal, gcController.lastHeapGoal)
// Update GC trigger and pacing, as well as downstream consumers
// of this pacing information, for the next cycle.
systemstack(gcControllerCommit)

// Update timing memstats
now := nanotime()
Expand Down Expand Up @@ -1111,7 +1110,7 @@ func gcMarkTermination() {
}
print(" ms cpu, ",
work.heap0>>20, "->", work.heap1>>20, "->", work.heap2>>20, " MB, ",
work.heapGoal>>20, " MB goal, ",
gcController.heapGoal()>>20, " MB goal, ",
gcController.stackScan>>20, " MB stacks, ",
gcController.globalsScan>>20, " MB globals, ",
work.maxprocs, " P")
Expand Down
Loading

0 comments on commit 129dcb7

Please sign in to comment.