mirror of
https://github.com/golang/go
synced 2024-09-06 00:19:45 +00:00
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/9177 Reviewed-by: Rick Hudson <rlh@golang.org>
This commit is contained in:
parent
91318dc76c
commit
4655aadd00
|
@ -332,10 +332,11 @@ func (c *gcControllerState) startCycle() {
|
|||
// error response).
|
||||
if memstats.next_gc <= heapminimum {
|
||||
memstats.heap_marked = uint64(float64(memstats.next_gc) / (1 + c.triggerRatio))
|
||||
memstats.heap_reachable = memstats.heap_marked
|
||||
}
|
||||
|
||||
// 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
|
||||
// dedicated and fractional workers.
|
||||
|
@ -1117,11 +1118,21 @@ func gcMark(start_time int64) {
|
|||
|
||||
cachestats()
|
||||
|
||||
// Trigger the next GC cycle when the allocated heap has
|
||||
// grown by triggerRatio over the marked heap size.
|
||||
// Compute the reachable heap size at the beginning of the
|
||||
// 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_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 {
|
||||
memstats.next_gc = heapminimum
|
||||
}
|
||||
|
|
|
@ -74,6 +74,10 @@ type mstats struct {
|
|||
// unlike heap_live, heap_marked does not change until the
|
||||
// next mark termination.
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue