Skip to content

Commit fc116b6

Browse files
committed
runtime: try to elide timer stealing if P has no timers
Following golang.org/cl/259578, findrunnable still must touch every other P in checkTimers in order to look for timers to steal. This scales poorly with GOMAXPROCS and potentially performs poorly by pulling remote Ps into cache. Add timerpMask, a bitmask that tracks whether each P may have any timers on its timer heap. Ideally we would update this field on any timer add / remove to always keep it up to date. Unfortunately, updating a shared global structure is antithetical to sharding timers by P, and doing so approximately doubles the cost of addtimer / deltimer in microbenchmarks. Instead we only (potentially) clear the mask when the P goes idle. This covers the best case of avoiding looking at a P _at all_ when it is idle and has no timers. See the comment on updateTimerPMask for more details on the trade-off. Future CLs may be able to expand cases we can avoid looking at the timers. Note that the addition of idlepMask to p.init is a no-op. The zero value of the mask is the correct init value so it is not necessary, but it is included for clarity. Benchmark results from WakeupParallel/syscall/pair/race/1ms (see golang.org/cl/228577). Note that these are on top of golang.org/cl/259578: name old msec new msec delta Perf-task-clock-8 244 ± 4% 246 ± 4% ~ (p=0.841 n=5+5) Perf-task-clock-16 247 ±11% 252 ± 4% ~ (p=1.000 n=5+5) Perf-task-clock-32 270 ± 1% 268 ± 2% ~ (p=0.548 n=5+5) Perf-task-clock-64 302 ± 3% 296 ± 1% ~ (p=0.222 n=5+5) Perf-task-clock-128 358 ± 3% 352 ± 2% ~ (p=0.310 n=5+5) Perf-task-clock-256 483 ± 3% 458 ± 1% -5.16% (p=0.008 n=5+5) Perf-task-clock-512 663 ± 1% 612 ± 4% -7.61% (p=0.008 n=5+5) Perf-task-clock-1024 1.06k ± 1% 0.95k ± 2% -10.24% (p=0.008 n=5+5) Updates #28808 Updates #18237 Change-Id: I4239cd89f21ad16dfbbef58d81981da48acd0605 Reviewed-on: https://go-review.googlesource.com/c/go/+/264477 Run-TryBot: Michael Pratt <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> Trust: Michael Pratt <[email protected]>
1 parent 642329f commit fc116b6

File tree

2 files changed

+81
-23
lines changed

2 files changed

+81
-23
lines changed

src/runtime/proc.go

+67-20
Original file line numberDiff line numberDiff line change
@@ -2512,9 +2512,9 @@ top:
25122512
// the timers for each P more than once with the same value of now
25132513
// is probably a waste of time.
25142514
//
2515-
// TODO(prattmic): Maintain a global look-aside similar to idlepMask
2516-
// to avoid looking at p2 if it can't possibly have timers.
2517-
if stealTimersOrRunNextG {
2515+
// timerpMask tells us whether the P may have timers at all. If it
2516+
// can't, no need to check at all.
2517+
if stealTimersOrRunNextG && timerpMask.read(enum.position()) {
25182518
tnow, w, ran := checkTimers(p2, now)
25192519
now = tnow
25202520
if w != 0 && (pollUntil == 0 || w < pollUntil) {
@@ -4502,6 +4502,13 @@ func (pp *p) init(id int32) {
45024502
}
45034503
}
45044504
lockInit(&pp.timersLock, lockRankTimers)
4505+
4506+
// This P may get timers when it starts running. Set the mask here
4507+
// since the P may not go through pidleget (notably P 0 on startup).
4508+
timerpMask.set(id)
4509+
// Similarly, we may not go through pidleget before this P starts
4510+
// running if it is P 0 on startup.
4511+
idlepMask.clear(id)
45054512
}
45064513

45074514
// destroy releases all of the resources associated with pp and
@@ -4647,11 +4654,16 @@ func procresize(nprocs int32) *p {
46474654

46484655
if maskWords <= int32(cap(idlepMask)) {
46494656
idlepMask = idlepMask[:maskWords]
4657+
timerpMask = timerpMask[:maskWords]
46504658
} else {
46514659
nidlepMask := make([]uint32, maskWords)
46524660
// No need to copy beyond len, old Ps are irrelevant.
46534661
copy(nidlepMask, idlepMask)
46544662
idlepMask = nidlepMask
4663+
4664+
ntimerpMask := make([]uint32, maskWords)
4665+
copy(ntimerpMask, timerpMask)
4666+
timerpMask = ntimerpMask
46554667
}
46564668
unlock(&allpLock)
46574669
}
@@ -4712,6 +4724,7 @@ func procresize(nprocs int32) *p {
47124724
lock(&allpLock)
47134725
allp = allp[:nprocs]
47144726
idlepMask = idlepMask[:maskWords]
4727+
timerpMask = timerpMask[:maskWords]
47154728
unlock(&allpLock)
47164729
}
47174730

@@ -5408,39 +5421,70 @@ func globrunqget(_p_ *p, max int32) *g {
54085421
return gp
54095422
}
54105423

5411-
// pIdleMask is a bitmap of of Ps in the _Pidle list, one bit per P.
5412-
type pIdleMask []uint32
5424+
// pMask is an atomic bitstring with one bit per P.
5425+
type pMask []uint32
54135426

5414-
// read returns true if P id is in the _Pidle list, and thus cannot have work.
5415-
func (p pIdleMask) read(id uint32) bool {
5427+
// read returns true if P id's bit is set.
5428+
func (p pMask) read(id uint32) bool {
54165429
word := id / 32
54175430
mask := uint32(1) << (id % 32)
54185431
return (atomic.Load(&p[word]) & mask) != 0
54195432
}
54205433

5421-
// set sets P id as idle in mask.
5422-
//
5423-
// Must be called only for a P owned by the caller. In order to maintain
5424-
// consistency, a P going idle must the idle mask simultaneously with updates
5425-
// to the idle P list under the sched.lock, otherwise a racing pidleget may
5426-
// clear the mask before pidleput sets the mask, corrupting the bitmap.
5427-
//
5428-
// N.B., procresize takes ownership of all Ps in stopTheWorldWithSema.
5429-
func (p pIdleMask) set(id int32) {
5434+
// set sets P id's bit.
5435+
func (p pMask) set(id int32) {
54305436
word := id / 32
54315437
mask := uint32(1) << (id % 32)
54325438
atomic.Or(&p[word], mask)
54335439
}
54345440

5435-
// clear sets P id as non-idle in mask.
5436-
//
5437-
// See comment on set.
5438-
func (p pIdleMask) clear(id int32) {
5441+
// clear clears P id's bit.
5442+
func (p pMask) clear(id int32) {
54395443
word := id / 32
54405444
mask := uint32(1) << (id % 32)
54415445
atomic.And(&p[word], ^mask)
54425446
}
54435447

5448+
// updateTimerPMask clears pp's timer mask if it has no timers on its heap.
5449+
//
5450+
// Ideally, the timer mask would be kept immediately consistent on any timer
5451+
// operations. Unfortunately, updating a shared global data structure in the
5452+
// timer hot path adds too much overhead in applications frequently switching
5453+
// between no timers and some timers.
5454+
//
5455+
// As a compromise, the timer mask is updated only on pidleget / pidleput. A
5456+
// running P (returned by pidleget) may add a timer at any time, so its mask
5457+
// must be set. An idle P (passed to pidleput) cannot add new timers while
5458+
// idle, so if it has no timers at that time, its mask may be cleared.
5459+
//
5460+
// Thus, we get the following effects on timer-stealing in findrunnable:
5461+
//
5462+
// * Idle Ps with no timers when they go idle are never checked in findrunnable
5463+
// (for work- or timer-stealing; this is the ideal case).
5464+
// * Running Ps must always be checked.
5465+
// * Idle Ps whose timers are stolen must continue to be checked until they run
5466+
// again, even after timer expiration.
5467+
//
5468+
// When the P starts running again, the mask should be set, as a timer may be
5469+
// added at any time.
5470+
//
5471+
// TODO(prattmic): Additional targeted updates may improve the above cases.
5472+
// e.g., updating the mask when stealing a timer.
5473+
func updateTimerPMask(pp *p) {
5474+
if atomic.Load(&pp.numTimers) > 0 {
5475+
return
5476+
}
5477+
5478+
// Looks like there are no timers, however another P may transiently
5479+
// decrement numTimers when handling a timerModified timer in
5480+
// checkTimers. We must take timersLock to serialize with these changes.
5481+
lock(&pp.timersLock)
5482+
if atomic.Load(&pp.numTimers) == 0 {
5483+
timerpMask.clear(pp.id)
5484+
}
5485+
unlock(&pp.timersLock)
5486+
}
5487+
54445488
// pidleput puts p to on the _Pidle list.
54455489
//
54465490
// This releases ownership of p. Once sched.lock is released it is no longer
@@ -5456,6 +5500,7 @@ func pidleput(_p_ *p) {
54565500
if !runqempty(_p_) {
54575501
throw("pidleput: P has non-empty run queue")
54585502
}
5503+
updateTimerPMask(_p_) // clear if there are no timers.
54595504
idlepMask.set(_p_.id)
54605505
_p_.link = sched.pidle
54615506
sched.pidle.set(_p_)
@@ -5473,6 +5518,8 @@ func pidleget() *p {
54735518

54745519
_p_ := sched.pidle.ptr()
54755520
if _p_ != nil {
5521+
// Timer may get added at any time now.
5522+
timerpMask.set(_p_.id)
54765523
idlepMask.clear(_p_.id)
54775524
sched.pidle = _p_.link
54785525
atomic.Xadd(&sched.npidle, -1) // TODO: fast atomic

src/runtime/runtime2.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -1052,15 +1052,26 @@ var (
10521052
sched schedt
10531053
newprocs int32
10541054

1055-
// allpLock protects P-less reads and size changes of allp and
1056-
// idlepMask, and all writes to allp.
1055+
// allpLock protects P-less reads and size changes of allp, idlepMask,
1056+
// and timerpMask, and all writes to allp.
10571057
allpLock mutex
10581058
// len(allp) == gomaxprocs; may change at safe points, otherwise
10591059
// immutable.
10601060
allp []*p
10611061
// Bitmask of Ps in _Pidle list, one bit per P. Reads and writes must
10621062
// be atomic. Length may change at safe points.
1063-
idlepMask pIdleMask
1063+
//
1064+
// Each P must update only its own bit. In order to maintain
1065+
// consistency, a P going idle must the idle mask simultaneously with
1066+
// updates to the idle P list under the sched.lock, otherwise a racing
1067+
// pidleget may clear the mask before pidleput sets the mask,
1068+
// corrupting the bitmap.
1069+
//
1070+
// N.B., procresize takes ownership of all Ps in stopTheWorldWithSema.
1071+
idlepMask pMask
1072+
// Bitmask of Ps that may have a timer, one bit per P. Reads and writes
1073+
// must be atomic. Length may change at safe points.
1074+
timerpMask pMask
10641075

10651076
// Information about what cpu features are available.
10661077
// Packages outside the runtime should not use these

0 commit comments

Comments
 (0)