Skip to content

Commit 49ea920

Browse files
committed
runtime: exit idle worker if there's higher-priority work
Idle GC workers trigger whenever there's a GC running and the scheduler doesn't find any other work. However, they currently run for a full scheduler quantum (~10ms) once started. This is really bad for event-driven applications, where work may come in on the network hundreds of times during that window. In the go-gcbench rpc benchmark, this is bad enough to often cause effective STWs where all Ps are in the idle worker. When this happens, we don't even poll the network any more (except for the background 10ms poll in sysmon), so we don't even know there's more work to do. Fix this by making idle workers check with the scheduler roughly every 100 µs to see if there's any higher-priority work the P should be doing. This check includes polling the network for incoming work. Fixes #16528. Change-Id: I6f62ebf6d36a92368da9891bafbbfd609b9bd003 Reviewed-on: https://go-review.googlesource.com/32433 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rick Hudson <[email protected]>
1 parent 7dc97d9 commit 49ea920

File tree

3 files changed

+55
-4
lines changed

3 files changed

+55
-4
lines changed

src/runtime/mgc.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1497,8 +1497,10 @@ func gcBgMarkWorker(_p_ *p) {
14971497
throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
14981498
case gcMarkWorkerDedicatedMode:
14991499
gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
1500-
case gcMarkWorkerFractionalMode, gcMarkWorkerIdleMode:
1500+
case gcMarkWorkerFractionalMode:
15011501
gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
1502+
case gcMarkWorkerIdleMode:
1503+
gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
15021504
}
15031505
casgstatus(gp, _Gwaiting, _Grunning)
15041506
})

src/runtime/mgcmark.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const (
3333
// This must be > _MaxSmallSize so that the object base is the
3434
// span base.
3535
maxObletBytes = 128 << 10
36+
37+
// idleCheckThreshold specifies how many units of work to do
38+
// between run queue checks in an idle worker. Assuming a scan
39+
// rate of 1 MB/ms, this is ~100 µs. Lower values have higher
40+
// overhead in the scan loop (the scheduler check may perform
41+
// a syscall, so its overhead is nontrivial). Higher values
42+
// make the system less responsive to incoming work.
43+
idleCheckThreshold = 100000
3644
)
3745

3846
// gcMarkRootPrepare queues root scanning jobs (stacks, globals, and
@@ -991,6 +999,7 @@ const (
991999
gcDrainUntilPreempt gcDrainFlags = 1 << iota
9921000
gcDrainNoBlock
9931001
gcDrainFlushBgCredit
1002+
gcDrainIdle
9941003

9951004
// gcDrainBlock means neither gcDrainUntilPreempt or
9961005
// gcDrainNoBlock. It is the default, but callers should use
@@ -1004,6 +1013,9 @@ const (
10041013
// If flags&gcDrainUntilPreempt != 0, gcDrain returns when g.preempt
10051014
// is set. This implies gcDrainNoBlock.
10061015
//
1016+
// If flags&gcDrainIdle != 0, gcDrain returns when there is other work
1017+
// to do. This implies gcDrainNoBlock.
1018+
//
10071019
// If flags&gcDrainNoBlock != 0, gcDrain returns as soon as it is
10081020
// unable to get more work. Otherwise, it will block until all
10091021
// blocking calls are blocked in gcDrain.
@@ -1020,8 +1032,14 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) {
10201032

10211033
gp := getg().m.curg
10221034
preemptible := flags&gcDrainUntilPreempt != 0
1023-
blocking := flags&(gcDrainUntilPreempt|gcDrainNoBlock) == 0
1035+
blocking := flags&(gcDrainUntilPreempt|gcDrainIdle|gcDrainNoBlock) == 0
10241036
flushBgCredit := flags&gcDrainFlushBgCredit != 0
1037+
idle := flags&gcDrainIdle != 0
1038+
1039+
initScanWork := gcw.scanWork
1040+
// idleCheck is the scan work at which to perform the next
1041+
// idle check with the scheduler.
1042+
idleCheck := initScanWork + idleCheckThreshold
10251043

10261044
// Drain root marking jobs.
10271045
if work.markrootNext < work.markrootJobs {
@@ -1031,11 +1049,12 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) {
10311049
break
10321050
}
10331051
markroot(gcw, job)
1052+
if idle && pollWork() {
1053+
goto done
1054+
}
10341055
}
10351056
}
10361057

1037-
initScanWork := gcw.scanWork
1038-
10391058
// Drain heap marking jobs.
10401059
for !(preemptible && gp.preempt) {
10411060
// Try to keep work available on the global queue. We used to
@@ -1071,14 +1090,23 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) {
10711090
gcFlushBgCredit(gcw.scanWork - initScanWork)
10721091
initScanWork = 0
10731092
}
1093+
idleCheck -= gcw.scanWork
10741094
gcw.scanWork = 0
1095+
1096+
if idle && idleCheck <= 0 {
1097+
idleCheck += idleCheckThreshold
1098+
if pollWork() {
1099+
break
1100+
}
1101+
}
10751102
}
10761103
}
10771104

10781105
// In blocking mode, write barriers are not allowed after this
10791106
// point because we must preserve the condition that the work
10801107
// buffers are empty.
10811108

1109+
done:
10821110
// Flush remaining scan work credit.
10831111
if gcw.scanWork > 0 {
10841112
atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)

src/runtime/proc.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,6 +2053,27 @@ stop:
20532053
goto top
20542054
}
20552055

2056+
// pollWork returns true if there is non-background work this P could
2057+
// be doing. This is a fairly lightweight check to be used for
2058+
// background work loops, like idle GC. It checks a subset of the
2059+
// conditions checked by the actual scheduler.
2060+
func pollWork() bool {
2061+
if sched.runqsize != 0 {
2062+
return true
2063+
}
2064+
p := getg().m.p.ptr()
2065+
if !runqempty(p) {
2066+
return true
2067+
}
2068+
if netpollinited() && sched.lastpoll != 0 {
2069+
if gp := netpoll(false); gp != nil {
2070+
injectglist(gp)
2071+
return true
2072+
}
2073+
}
2074+
return false
2075+
}
2076+
20562077
func resetspinning() {
20572078
_g_ := getg()
20582079
if !_g_.m.spinning {

0 commit comments

Comments
 (0)