Commit 4655aadd authored by Austin Clements's avatar Austin Clements

runtime: use reachable heap estimate to set trigger/goal

Currently, we set the heap goal for the next GC cycle using the size
of the marked heap at the end of the current cycle. This can lead to a
bad feedback loop if the mutator is rapidly allocating and releasing
pointers that can significantly bloat heap size.

If the GC were STW, the marked heap size would be exactly the
reachable heap size (call it stwLive). However, in concurrent GC,
marked=stwLive+floatLive, where floatLive is the amount of "floating
garbage": objects that were reachable at some point during the cycle
and were marked, but which are no longer reachable by the end of the
cycle. If the GC cycle is short, then the mutator doesn't have much
time to create floating garbage, so marked≈stwLive. However, if the GC
cycle is long and the mutator is allocating and creating floating
garbage very rapidly, then it's possible that marked≫stwLive. Since
the runtime currently sets the heap goal based on marked, this will
cause it to set a high heap goal. This means that 1) the next GC cycle
will take longer because of the larger heap and 2) the assist ratio
will be low because of the large distance between the trigger and the
goal. The combination of these lets the mutator produce even more
floating garbage in the next cycle, which further exacerbates the
problem.

For example, on the garbage benchmark with GOMAXPROCS=1, this causes
the heap to grow to ~500MB and the garbage collector to retain upwards
of ~300MB of heap, while the true reachable heap size is ~32MB. This,
in turn, causes the GC cycle to take upwards of ~3 seconds.

Fix this bad feedback loop by estimating the true reachable heap size
(stwLive) and using this rather than the marked heap size
(stwLive+floatLive) as the basis for the GC trigger and heap goal.
This breaks the bad feedback loop and causes the mutator to assist
more, which decreases the rate at which it can create floating
garbage. On the same garbage benchmark, this reduces the maximum heap
size to ~73MB, the retained heap to ~40MB, and the duration of the GC
cycle to ~200ms.

Change-Id: I7712244c94240743b266f9eb720c03802799cdd1
Reviewed-on: https://go-review.googlesource.com/9177Reviewed-by: 's avatarRick Hudson <rlh@golang.org>
parent 91318dc7
...@@ -332,10 +332,11 @@ func (c *gcControllerState) startCycle() { ...@@ -332,10 +332,11 @@ func (c *gcControllerState) startCycle() {
// error response). // error response).
if memstats.next_gc <= heapminimum { if memstats.next_gc <= heapminimum {
memstats.heap_marked = uint64(float64(memstats.next_gc) / (1 + c.triggerRatio)) memstats.heap_marked = uint64(float64(memstats.next_gc) / (1 + c.triggerRatio))
memstats.heap_reachable = memstats.heap_marked
} }
// Compute the heap goal for this cycle // Compute the heap goal for this cycle
c.heapGoal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100 c.heapGoal = memstats.heap_reachable + memstats.heap_reachable*uint64(gcpercent)/100
// Compute the total mark utilization goal and divide it among // Compute the total mark utilization goal and divide it among
// dedicated and fractional workers. // dedicated and fractional workers.
...@@ -1117,11 +1118,21 @@ func gcMark(start_time int64) { ...@@ -1117,11 +1118,21 @@ func gcMark(start_time int64) {
cachestats() cachestats()
// Trigger the next GC cycle when the allocated heap has // Compute the reachable heap size at the beginning of the
// grown by triggerRatio over the marked heap size. // cycle. This is approximately the marked heap size at the
// end (which we know) minus the amount of marked heap that
// was allocated after marking began (which we don't know, but
// is approximately the amount of heap that was allocated
// since marking began).
memstats.heap_reachable = work.bytesMarked - (memstats.heap_live - gcController.initialHeapLive)
// Trigger the next GC cycle when the allocated heap has grown
// by triggerRatio over the reachable heap size. Assume that
// we're in steady state, so the reachable heap size is the
// same now as it was at the beginning of the GC cycle.
memstats.heap_live = work.bytesMarked memstats.heap_live = work.bytesMarked
memstats.heap_marked = work.bytesMarked memstats.heap_marked = work.bytesMarked
memstats.next_gc = uint64(float64(memstats.heap_live) * (1 + gcController.triggerRatio)) memstats.next_gc = uint64(float64(memstats.heap_reachable) * (1 + gcController.triggerRatio))
if memstats.next_gc < heapminimum { if memstats.next_gc < heapminimum {
memstats.next_gc = heapminimum memstats.next_gc = heapminimum
} }
......
...@@ -74,6 +74,10 @@ type mstats struct { ...@@ -74,6 +74,10 @@ type mstats struct {
// unlike heap_live, heap_marked does not change until the // unlike heap_live, heap_marked does not change until the
// next mark termination. // next mark termination.
heap_marked uint64 heap_marked uint64
// heap_reachable is an estimate of the reachable heap bytes
// at the end of the previous GC.
heap_reachable uint64
} }
var memstats mstats var memstats mstats
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment