Skip to content

Commit ec9c84c

Browse files
committed
runtime: disentangle next_gc from GC trigger
Back in Go 1.4, memstats.next_gc was both the heap size at which GC would trigger, and the size GC kept the heap under. When we switched to concurrent GC in Go 1.5, we got somewhat confused and made this variable the trigger heap size, while gcController.heapGoal became the goal heap size. memstats.next_gc is exposed to the user via MemStats.NextGC, while gcController.heapGoal is not. This is unfortunate because 1) the heap goal is far more useful for diagnostics, and 2) the trigger heap size is just part of the GC trigger heuristic, which means it wouldn't be useful to an application even if it tried to use it. We never noticed this mess because MemStats.NextGC is practically undocumented. Now that we're trying to document MemStats, it became clear that this field had diverged from its original usefulness. Clean up this mess by shuffling things back around so that next_gc is the goal heap size and the new (unexposed) memstats.gc_trigger field is the trigger heap size. This eliminates gcController.heapGoal. Updates #15849. Change-Id: I2cbbd43b1d78bdf613cb43f53488bd63913189b7 Reviewed-on: https://go-review.googlesource.com/29270 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Hyang-Ah Hana Kim <[email protected]> Reviewed-by: Rick Hudson <[email protected]>
1 parent 196df6f commit ec9c84c

File tree

2 files changed

+54
-33
lines changed

2 files changed

+54
-33
lines changed

src/runtime/mgc.go

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,13 @@ func gcinit() {
180180
datap.gcdatamask = progToPointerMask((*byte)(unsafe.Pointer(datap.gcdata)), datap.edata-datap.data)
181181
datap.gcbssmask = progToPointerMask((*byte)(unsafe.Pointer(datap.gcbss)), datap.ebss-datap.bss)
182182
}
183-
memstats.next_gc = heapminimum
183+
memstats.gc_trigger = heapminimum
184+
// Compute the goal heap size based on the trigger:
185+
// trigger = marked * (1 + triggerRatio)
186+
// marked = trigger / (1 + triggerRatio)
187+
// goal = marked * (1 + GOGC/100)
188+
// = trigger / (1 + triggerRatio) * (1 + GOGC/100)
189+
memstats.next_gc = uint64(float64(memstats.gc_trigger) / (1 + gcController.triggerRatio) * (1 + float64(gcpercent)/100))
184190
work.startSema = 1
185191
work.markDoneSema = 1
186192
}
@@ -218,6 +224,9 @@ func setGCPercent(in int32) (out int32) {
218224
if gcController.triggerRatio > float64(gcpercent)/100 {
219225
gcController.triggerRatio = float64(gcpercent) / 100
220226
}
227+
// This is either in gcinit or followed by a STW GC, both of
228+
// which will reset other stats like memstats.gc_trigger and
229+
// memstats.next_gc to appropriate values.
221230
unlock(&mheap_.lock)
222231
return out
223232
}
@@ -303,7 +312,7 @@ const (
303312
// when to trigger concurrent garbage collection and how much marking
304313
// work to do in mutator assists and background marking.
305314
//
306-
// It uses a feedback control algorithm to adjust the memstats.next_gc
315+
// It uses a feedback control algorithm to adjust the memstats.gc_trigger
307316
// trigger based on the heap growth and GC CPU utilization each cycle.
308317
// This algorithm optimizes for heap growth to match GOGC and for CPU
309318
// utilization between assist and background marking to be 25% of
@@ -359,10 +368,6 @@ type gcControllerState struct {
359368
// that assists and background mark workers started.
360369
markStartTime int64
361370

362-
// heapGoal is the goal memstats.heap_live for when this cycle
363-
// ends. This is computed at the beginning of each cycle.
364-
heapGoal uint64
365-
366371
// dedicatedMarkWorkersNeeded is the number of dedicated mark
367372
// workers that need to be started. This is computed at the
368373
// beginning of each cycle and decremented atomically as
@@ -390,8 +395,9 @@ type gcControllerState struct {
390395
// triggerRatio is the heap growth ratio at which the garbage
391396
// collection cycle should start. E.g., if this is 0.6, then
392397
// GC should start when the live heap has reached 1.6 times
393-
// the heap size marked by the previous cycle. This is updated
394-
// at the end of of each cycle.
398+
// the heap size marked by the previous cycle. This should be
399+
// ≤ GOGC/100 so the trigger heap size is less than the goal
400+
// heap size. This is updated at the end of of each cycle.
395401
triggerRatio float64
396402

397403
_ [sys.CacheLineSize]byte
@@ -416,28 +422,29 @@ func (c *gcControllerState) startCycle() {
416422
c.idleMarkTime = 0
417423

418424
// If this is the first GC cycle or we're operating on a very
419-
// small heap, fake heap_marked so it looks like next_gc is
425+
// small heap, fake heap_marked so it looks like gc_trigger is
420426
// the appropriate growth from heap_marked, even though the
421427
// real heap_marked may not have a meaningful value (on the
422428
// first cycle) or may be much smaller (resulting in a large
423429
// error response).
424-
if memstats.next_gc <= heapminimum {
425-
memstats.heap_marked = uint64(float64(memstats.next_gc) / (1 + c.triggerRatio))
430+
if memstats.gc_trigger <= heapminimum {
431+
memstats.heap_marked = uint64(float64(memstats.gc_trigger) / (1 + c.triggerRatio))
426432
memstats.heap_reachable = memstats.heap_marked
427433
}
428434

429-
// Compute the heap goal for this cycle
430-
c.heapGoal = memstats.heap_reachable + memstats.heap_reachable*uint64(gcpercent)/100
435+
// Re-compute the heap goal for this cycle in case something
436+
// changed. This is the same calculation we use elsewhere.
437+
memstats.next_gc = memstats.heap_reachable + memstats.heap_reachable*uint64(gcpercent)/100
431438

432439
// Ensure that the heap goal is at least a little larger than
433440
// the current live heap size. This may not be the case if GC
434441
// start is delayed or if the allocation that pushed heap_live
435-
// over next_gc is large or if the trigger is really close to
442+
// over gc_trigger is large or if the trigger is really close to
436443
// GOGC. Assist is proportional to this distance, so enforce a
437444
// minimum distance, even if it means going over the GOGC goal
438445
// by a tiny bit.
439-
if c.heapGoal < memstats.heap_live+1024*1024 {
440-
c.heapGoal = memstats.heap_live + 1024*1024
446+
if memstats.next_gc < memstats.heap_live+1024*1024 {
447+
memstats.next_gc = memstats.heap_live + 1024*1024
441448
}
442449

443450
// Compute the total mark utilization goal and divide it among
@@ -467,7 +474,7 @@ func (c *gcControllerState) startCycle() {
467474
print("pacer: assist ratio=", c.assistWorkPerByte,
468475
" (scan ", memstats.heap_scan>>20, " MB in ",
469476
work.initialHeapLive>>20, "->",
470-
c.heapGoal>>20, " MB)",
477+
memstats.next_gc>>20, " MB)",
471478
" workers=", c.dedicatedMarkWorkersNeeded,
472479
"+", c.fractionalMarkWorkersNeeded, "\n")
473480
}
@@ -516,7 +523,7 @@ func (c *gcControllerState) revise() {
516523
}
517524

518525
// Compute the heap distance remaining.
519-
heapDistance := int64(c.heapGoal) - int64(memstats.heap_live)
526+
heapDistance := int64(memstats.next_gc) - int64(memstats.heap_live)
520527
if heapDistance <= 0 {
521528
// This shouldn't happen, but if it does, avoid
522529
// dividing by zero or setting the assist negative.
@@ -552,7 +559,7 @@ func (c *gcControllerState) endCycle() {
552559
// difference between this estimate and the GOGC-based goal
553560
// heap growth is the error.
554561
//
555-
// TODO(austin): next_gc is based on heap_reachable, not
562+
// TODO(austin): gc_trigger is based on heap_reachable, not
556563
// heap_marked, which means the actual growth ratio
557564
// technically isn't comparable to the trigger ratio.
558565
goalGrowthRatio := float64(gcpercent) / 100
@@ -585,7 +592,7 @@ func (c *gcControllerState) endCycle() {
585592
// Print controller state in terms of the design
586593
// document.
587594
H_m_prev := memstats.heap_marked
588-
H_T := memstats.next_gc
595+
H_T := memstats.gc_trigger
589596
h_a := actualGrowthRatio
590597
H_a := memstats.heap_live
591598
h_g := goalGrowthRatio
@@ -881,7 +888,7 @@ const (
881888
// If forceTrigger is true, it ignores the current heap size, but
882889
// checks all other conditions. In general this should be false.
883890
func gcShouldStart(forceTrigger bool) bool {
884-
return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.next_gc) && memstats.enablegc && panicking == 0 && gcpercent >= 0
891+
return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
885892
}
886893

887894
// gcStart transitions the GC from _GCoff to _GCmark (if mode ==
@@ -979,7 +986,7 @@ func gcStart(mode gcMode, forceTrigger bool) {
979986

980987
if mode == gcBackgroundMode { // Do as much work concurrently as possible
981988
gcController.startCycle()
982-
work.heapGoal = gcController.heapGoal
989+
work.heapGoal = memstats.next_gc
983990

984991
// Enter concurrent mark phase and enable
985992
// write barriers.
@@ -1624,13 +1631,13 @@ func gcMark(start_time int64) {
16241631
// by triggerRatio over the reachable heap size. Assume that
16251632
// we're in steady state, so the reachable heap size is the
16261633
// same now as it was at the beginning of the GC cycle.
1627-
memstats.next_gc = uint64(float64(memstats.heap_reachable) * (1 + gcController.triggerRatio))
1628-
if memstats.next_gc < heapminimum {
1629-
memstats.next_gc = heapminimum
1634+
memstats.gc_trigger = uint64(float64(memstats.heap_reachable) * (1 + gcController.triggerRatio))
1635+
if memstats.gc_trigger < heapminimum {
1636+
memstats.gc_trigger = heapminimum
16301637
}
1631-
if int64(memstats.next_gc) < 0 {
1638+
if int64(memstats.gc_trigger) < 0 {
16321639
print("next_gc=", memstats.next_gc, " bytesMarked=", work.bytesMarked, " heap_live=", memstats.heap_live, " initialHeapLive=", work.initialHeapLive, "\n")
1633-
throw("next_gc underflow")
1640+
throw("gc_trigger underflow")
16341641
}
16351642

16361643
// Update other GC heap size stats. This must happen after
@@ -1640,19 +1647,26 @@ func gcMark(start_time int64) {
16401647
memstats.heap_marked = work.bytesMarked
16411648
memstats.heap_scan = uint64(gcController.scanWork)
16421649

1643-
minNextGC := memstats.heap_live + sweepMinHeapDistance*uint64(gcpercent)/100
1644-
if memstats.next_gc < minNextGC {
1650+
minTrigger := memstats.heap_live + sweepMinHeapDistance*uint64(gcpercent)/100
1651+
if memstats.gc_trigger < minTrigger {
16451652
// The allocated heap is already past the trigger.
16461653
// This can happen if the triggerRatio is very low and
16471654
// the reachable heap estimate is less than the live
16481655
// heap size.
16491656
//
16501657
// Concurrent sweep happens in the heap growth from
1651-
// heap_live to next_gc, so bump next_gc up to ensure
1658+
// heap_live to gc_trigger, so bump gc_trigger up to ensure
16521659
// that concurrent sweep has some heap growth in which
16531660
// to perform sweeping before we start the next GC
16541661
// cycle.
1655-
memstats.next_gc = minNextGC
1662+
memstats.gc_trigger = minTrigger
1663+
}
1664+
1665+
// The next GC cycle should finish before the allocated heap
1666+
// has grown by GOGC/100.
1667+
memstats.next_gc = memstats.heap_reachable + memstats.heap_reachable*uint64(gcpercent)/100
1668+
if memstats.next_gc < memstats.gc_trigger {
1669+
memstats.next_gc = memstats.gc_trigger
16561670
}
16571671

16581672
if trace.enabled {
@@ -1693,7 +1707,7 @@ func gcSweep(mode gcMode) {
16931707
// Concurrent sweep needs to sweep all of the in-use pages by
16941708
// the time the allocated heap reaches the GC trigger. Compute
16951709
// the ratio of in-use pages to sweep per byte allocated.
1696-
heapDistance := int64(memstats.next_gc) - int64(memstats.heap_live)
1710+
heapDistance := int64(memstats.gc_trigger) - int64(memstats.heap_live)
16971711
// Add a little margin so rounding errors and concurrent
16981712
// sweep are less likely to leave pages unswept when GC starts.
16991713
heapDistance -= 1024 * 1024

src/runtime/mstats.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type mstats struct {
4646

4747
// Statistics about garbage collector.
4848
// Protected by mheap or stopping the world during GC.
49-
next_gc uint64 // next gc (in heap_live time)
49+
next_gc uint64 // goal heap_live for when next GC ends
5050
last_gc uint64 // last gc (in absolute time)
5151
pause_total_ns uint64
5252
pause_ns [256]uint64 // circular buffer of recent gc pause lengths
@@ -68,6 +68,13 @@ type mstats struct {
6868

6969
tinyallocs uint64 // number of tiny allocations that didn't cause actual allocation; not exported to go directly
7070

71+
// gc_trigger is the heap size that triggers marking.
72+
//
73+
// When heap_live ≥ gc_trigger, the mark phase will start.
74+
// This is also the heap size by which proportional sweeping
75+
// must be complete.
76+
gc_trigger uint64
77+
7178
// heap_live is the number of bytes considered live by the GC.
7279
// That is: retained by the most recent GC plus allocated
7380
// since then. heap_live <= heap_alloc, since heap_alloc

0 commit comments

Comments
 (0)