Skip to content

Commit 28e1a8e

Browse files
committed
runtime: preempt fractional worker after reaching utilization goal
Currently fractional workers run until preempted by the scheduler, which means they typically run for 20ms. During this time, all other goroutines on that P are blocked, which can introduce significant latency variance. This modifies fractional workers to self-preempt shortly after achieving the fractional utilization goal. In practice this means they preempt much sooner, and the scale of their preemption is on the order of how often the user goroutine block (so, if the application is compute-bound, the fractional workers will also run for long times, but if the application blocks frequently, the fractional workers will also preempt quickly). Fixes #21698. Updates #18534. Change-Id: I03a5ab195dae93154a46c32083c4bb52415d2017 Reviewed-on: https://go-review.googlesource.com/68573 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rick Hudson <[email protected]>
1 parent b783930 commit 28e1a8e

File tree

3 files changed

+54
-17
lines changed

3 files changed

+54
-17
lines changed

src/runtime/mgc.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,8 @@ func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
711711
// This P has picked the token for the fractional worker.
712712
// Is the GC currently under or at the utilization goal?
713713
// If so, do more work.
714+
//
715+
// This should be kept in sync with pollFractionalWorkerExit.
714716

715717
// TODO(austin): We could fast path this and basically
716718
// eliminate contention on c.fractionalMarkWorkersNeeded by
@@ -719,10 +721,6 @@ func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
719721
// don't have to fight in the window where we've
720722
// passed that deadline and no one has started the
721723
// worker yet.
722-
//
723-
// TODO(austin): Shorter preemption interval for mark
724-
// worker to improve fairness and give this
725-
// finer-grained control over schedule?
726724
delta := nanotime() - c.markStartTime
727725
if delta > 0 && float64(c.fractionalMarkTime)/float64(delta) > c.fractionalUtilizationGoal {
728726
// Nope, we'd overshoot the utilization goal
@@ -741,6 +739,25 @@ func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
741739
return gp
742740
}
743741

742+
// pollFractionalWorkerExit returns true if a fractional mark worker
743+
// should self-preempt. It assumes it is called from the fractional
744+
// worker.
745+
func pollFractionalWorkerExit() bool {
746+
// This should be kept in sync with the fractional worker
747+
// scheduler logic in findRunnableGCWorker.
748+
now := nanotime()
749+
delta := now - gcController.markStartTime
750+
if delta <= 0 {
751+
return true
752+
}
753+
p := getg().m.p.ptr()
754+
// Account for time since starting this worker.
755+
selfTime := gcController.fractionalMarkTime + (now - p.gcMarkWorkerStartTime)
756+
// Add some slack to the utilization goal so that the
757+
// fractional worker isn't behind again the instant it exits.
758+
return float64(selfTime)/float64(delta) > 1.2*gcController.fractionalUtilizationGoal
759+
}
760+
744761
// gcSetTriggerRatio sets the trigger ratio and updates everything
745762
// derived from it: the absolute trigger, the heap goal, mark pacing,
746763
// and sweep pacing.
@@ -1765,6 +1782,7 @@ func gcBgMarkWorker(_p_ *p) {
17651782
}
17661783

17671784
startTime := nanotime()
1785+
_p_.gcMarkWorkerStartTime = startTime
17681786

17691787
decnwait := atomic.Xadd(&work.nwait, -1)
17701788
if decnwait == work.nproc {
@@ -1806,7 +1824,7 @@ func gcBgMarkWorker(_p_ *p) {
18061824
// without preemption.
18071825
gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
18081826
case gcMarkWorkerFractionalMode:
1809-
gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
1827+
gcDrain(&_p_.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)
18101828
case gcMarkWorkerIdleMode:
18111829
gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
18121830
}

src/runtime/mgcmark.go

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ const (
3434
// span base.
3535
maxObletBytes = 128 << 10
3636

37-
// idleCheckThreshold specifies how many units of work to do
38-
// between run queue checks in an idle worker. Assuming a scan
37+
// drainCheckThreshold specifies how many units of work to do
38+
// between self-preemption checks in gcDrain. Assuming a scan
3939
// rate of 1 MB/ms, this is ~100 µs. Lower values have higher
4040
// overhead in the scan loop (the scheduler check may perform
4141
// a syscall, so its overhead is nontrivial). Higher values
4242
// make the system less responsive to incoming work.
43-
idleCheckThreshold = 100000
43+
drainCheckThreshold = 100000
4444
)
4545

4646
// gcMarkRootPrepare queues root scanning jobs (stacks, globals, and
@@ -861,6 +861,7 @@ const (
861861
gcDrainNoBlock
862862
gcDrainFlushBgCredit
863863
gcDrainIdle
864+
gcDrainFractional
864865

865866
// gcDrainBlock means neither gcDrainUntilPreempt or
866867
// gcDrainNoBlock. It is the default, but callers should use
@@ -877,6 +878,10 @@ const (
877878
// If flags&gcDrainIdle != 0, gcDrain returns when there is other work
878879
// to do. This implies gcDrainNoBlock.
879880
//
881+
// If flags&gcDrainFractional != 0, gcDrain self-preempts when
882+
// pollFractionalWorkerExit() returns true. This implies
883+
// gcDrainNoBlock.
884+
//
880885
// If flags&gcDrainNoBlock != 0, gcDrain returns as soon as it is
881886
// unable to get more work. Otherwise, it will block until all
882887
// blocking calls are blocked in gcDrain.
@@ -893,14 +898,24 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) {
893898

894899
gp := getg().m.curg
895900
preemptible := flags&gcDrainUntilPreempt != 0
896-
blocking := flags&(gcDrainUntilPreempt|gcDrainIdle|gcDrainNoBlock) == 0
901+
blocking := flags&(gcDrainUntilPreempt|gcDrainIdle|gcDrainFractional|gcDrainNoBlock) == 0
897902
flushBgCredit := flags&gcDrainFlushBgCredit != 0
898903
idle := flags&gcDrainIdle != 0
899904

900905
initScanWork := gcw.scanWork
901-
// idleCheck is the scan work at which to perform the next
902-
// idle check with the scheduler.
903-
idleCheck := initScanWork + idleCheckThreshold
906+
907+
// checkWork is the scan work before performing the next
908+
// self-preempt check.
909+
checkWork := int64(1<<63 - 1)
910+
var check func() bool
911+
if flags&(gcDrainIdle|gcDrainFractional) != 0 {
912+
checkWork = initScanWork + drainCheckThreshold
913+
if idle {
914+
check = pollWork
915+
} else if flags&gcDrainFractional != 0 {
916+
check = pollFractionalWorkerExit
917+
}
918+
}
904919

905920
// Drain root marking jobs.
906921
if work.markrootNext < work.markrootJobs {
@@ -910,7 +925,7 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) {
910925
break
911926
}
912927
markroot(gcw, job)
913-
if idle && pollWork() {
928+
if check != nil && check() {
914929
goto done
915930
}
916931
}
@@ -951,12 +966,12 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) {
951966
gcFlushBgCredit(gcw.scanWork - initScanWork)
952967
initScanWork = 0
953968
}
954-
idleCheck -= gcw.scanWork
969+
checkWork -= gcw.scanWork
955970
gcw.scanWork = 0
956971

957-
if idle && idleCheck <= 0 {
958-
idleCheck += idleCheckThreshold
959-
if pollWork() {
972+
if checkWork <= 0 {
973+
checkWork += drainCheckThreshold
974+
if check != nil && check() {
960975
break
961976
}
962977
}

src/runtime/runtime2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,10 @@ type p struct {
526526
gcBgMarkWorker guintptr
527527
gcMarkWorkerMode gcMarkWorkerMode
528528

529+
// gcMarkWorkerStartTime is the nanotime() at which this mark
530+
// worker started.
531+
gcMarkWorkerStartTime int64
532+
529533
// gcw is this P's GC work buffer cache. The work buffer is
530534
// filled by write barriers, drained by mutator assists, and
531535
// disposed on certain GC state transitions.

0 commit comments

Comments
 (0)