Skip to content

Commit a136919

Browse files
committed
runtime: add new mcentral implementation
Currently mcentral is implemented as a couple of linked lists of spans protected by a lock. Unfortunately this design leads to significant lock contention. The span ownership model is also confusing and complicated. In-use spans jump between being owned by multiple sources, generally some combination of a gcSweepBuf, a concurrent sweeper, an mcentral or an mcache. So first to address contention, this change replaces those linked lists with gcSweepBufs which have an atomic fast path. Then, we change up the ownership model: a span may be simultaneously owned only by an mcentral and the page reclaimer. Otherwise, an mcentral (which now consists of sweep bufs), a sweeper, or an mcache are the sole owners of a span at any given time. This dramatically simplifies reasoning about span ownership in the runtime. As a result of this new ownership model, sweeping is now driven by walking over the mcentrals rather than having its own global list of spans. Because we no longer have a global list and we traditionally haven't used the mcentrals for large object spans, we no longer have anywhere to put large objects. So, this change also makes it so that we keep large object spans in the appropriate mcentral lists. In terms of the static lock ranking, we add the spanSet spine locks in pretty much the same place as the mcentral locks, since they have the potential to be manipulated both on the allocation and sweep paths, like the mcentral locks. This new implementation is turned on by default via a feature flag called go115NewMCentralImpl. Benchmark results for 1 KiB allocation throughput (5 runs each): name \ MiB/s go113 go114 gotip gotip+this-patch AllocKiB-1 1.71k ± 1% 1.68k ± 1% 1.59k ± 2% 1.71k ± 1% AllocKiB-2 2.46k ± 1% 2.51k ± 1% 2.54k ± 1% 2.93k ± 1% AllocKiB-4 4.27k ± 1% 4.41k ± 2% 4.33k ± 1% 5.01k ± 2% AllocKiB-8 4.38k ± 3% 5.24k ± 1% 5.46k ± 1% 8.23k ± 1% AllocKiB-12 4.38k ± 3% 4.49k ± 1% 5.10k ± 1% 10.04k ± 0% AllocKiB-16 4.31k ± 1% 4.14k ± 3% 4.22k ± 0% 10.42k ± 0% AllocKiB-20 4.26k ± 1% 3.98k ± 1% 4.09k ± 1% 10.46k ± 3% AllocKiB-24 4.20k ± 1% 3.97k ± 1% 4.06k ± 1% 10.74k ± 1% AllocKiB-28 4.15k ± 0% 4.00k ± 0% 4.20k ± 0% 10.76k ± 1% Fixes #37487. Change-Id: I92d47355acacf9af2c41bf080c08a8c1638ba210 Reviewed-on: https://go-review.googlesource.com/c/go/+/221182 Run-TryBot: Michael Knyszek <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Austin Clements <[email protected]>
1 parent 9582b6e commit a136919

File tree

7 files changed

+620
-29
lines changed

7 files changed

+620
-29
lines changed

src/runtime/lockrank.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ const (
6666
lockRankRwmutexW
6767
lockRankRwmutexR
6868

69-
lockRankMcentral
70-
lockRankSpine
69+
lockRankMcentral // For !go115NewMCentralImpl
70+
lockRankSpine // For !go115NewMCentralImpl
71+
lockRankSpanSetSpine
7172
lockRankStackpool
7273
lockRankStackLarge
7374
lockRankDefer
@@ -137,12 +138,13 @@ var lockNames = []string{
137138
lockRankRwmutexW: "rwmutexW",
138139
lockRankRwmutexR: "rwmutexR",
139140

140-
lockRankMcentral: "mcentral",
141-
lockRankSpine: "spine",
142-
lockRankStackpool: "stackpool",
143-
lockRankStackLarge: "stackLarge",
144-
lockRankDefer: "defer",
145-
lockRankSudog: "sudog",
141+
lockRankMcentral: "mcentral",
142+
lockRankSpine: "spine",
143+
lockRankSpanSetSpine: "spanSetSpine",
144+
lockRankStackpool: "stackpool",
145+
lockRankStackLarge: "stackLarge",
146+
lockRankDefer: "defer",
147+
lockRankSudog: "sudog",
146148

147149
lockRankWbufSpans: "wbufSpans",
148150
lockRankMheap: "mheap",
@@ -214,14 +216,15 @@ var lockPartialOrder [][]lockRank = [][]lockRank{
214216

215217
lockRankMcentral: {lockRankScavenge, lockRankForcegc, lockRankAssistQueue, lockRankCpuprof, lockRankSweep, lockRankSched, lockRankAllg, lockRankAllp, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankHchan},
216218
lockRankSpine: {lockRankScavenge, lockRankCpuprof, lockRankSched, lockRankAllg, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankHchan},
217-
lockRankStackpool: {lockRankScavenge, lockRankSweepWaiters, lockRankAssistQueue, lockRankCpuprof, lockRankSweep, lockRankSched, lockRankPollDesc, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankHchan, lockRankFin, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankProf, lockRankGcBitsArenas, lockRankRoot, lockRankTrace, lockRankTraceStackTab, lockRankNetpollInit, lockRankRwmutexR, lockRankMcentral, lockRankSpine},
218-
lockRankStackLarge: {lockRankAssistQueue, lockRankSched, lockRankItab, lockRankHchan, lockRankProf, lockRankGcBitsArenas, lockRankRoot, lockRankMcentral},
219+
lockRankSpanSetSpine: {lockRankScavenge, lockRankForcegc, lockRankAssistQueue, lockRankCpuprof, lockRankSweep, lockRankSched, lockRankAllg, lockRankAllp, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankHchan},
220+
lockRankStackpool: {lockRankScavenge, lockRankSweepWaiters, lockRankAssistQueue, lockRankCpuprof, lockRankSweep, lockRankSched, lockRankPollDesc, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankHchan, lockRankFin, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankProf, lockRankGcBitsArenas, lockRankRoot, lockRankTrace, lockRankTraceStackTab, lockRankNetpollInit, lockRankRwmutexR, lockRankMcentral, lockRankSpine, lockRankSpanSetSpine},
221+
lockRankStackLarge: {lockRankAssistQueue, lockRankSched, lockRankItab, lockRankHchan, lockRankProf, lockRankGcBitsArenas, lockRankRoot, lockRankMcentral, lockRankSpanSetSpine},
219222
lockRankDefer: {},
220223
lockRankSudog: {lockRankNotifyList, lockRankHchan},
221224
lockRankWbufSpans: {lockRankScavenge, lockRankSweepWaiters, lockRankAssistQueue, lockRankSweep, lockRankSched, lockRankAllg, lockRankPollDesc, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankHchan, lockRankNotifyList, lockRankTraceStrings, lockRankMspanSpecial, lockRankProf, lockRankRoot, lockRankDefer, lockRankSudog},
222-
lockRankMheap: {lockRankScavenge, lockRankSweepWaiters, lockRankAssistQueue, lockRankCpuprof, lockRankSweep, lockRankSched, lockRankAllg, lockRankAllp, lockRankPollDesc, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankHchan, lockRankMspanSpecial, lockRankProf, lockRankGcBitsArenas, lockRankRoot, lockRankMcentral, lockRankStackpool, lockRankStackLarge, lockRankDefer, lockRankSudog, lockRankWbufSpans},
225+
lockRankMheap: {lockRankScavenge, lockRankSweepWaiters, lockRankAssistQueue, lockRankCpuprof, lockRankSweep, lockRankSched, lockRankAllg, lockRankAllp, lockRankPollDesc, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankHchan, lockRankMspanSpecial, lockRankProf, lockRankGcBitsArenas, lockRankRoot, lockRankMcentral, lockRankStackpool, lockRankStackLarge, lockRankDefer, lockRankSudog, lockRankWbufSpans, lockRankSpanSetSpine},
223226
lockRankMheapSpecial: {lockRankScavenge, lockRankCpuprof, lockRankSched, lockRankAllg, lockRankAllp, lockRankTimers, lockRankItab, lockRankReflectOffs, lockRankNotifyList, lockRankTraceBuf, lockRankTraceStrings, lockRankHchan},
224-
lockRankGlobalAlloc: {lockRankProf, lockRankSpine, lockRankMheap, lockRankMheapSpecial},
227+
lockRankGlobalAlloc: {lockRankProf, lockRankSpine, lockRankSpanSetSpine, lockRankMheap, lockRankMheapSpecial},
225228

226229
lockRankGFree: {lockRankSched},
227230

src/runtime/malloc.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1171,10 +1171,16 @@ func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
11711171
// pays the debt down to npage pages.
11721172
deductSweepCredit(npages*_PageSize, npages)
11731173

1174-
s := mheap_.alloc(npages, makeSpanClass(0, noscan), needzero)
1174+
spc := makeSpanClass(0, noscan)
1175+
s := mheap_.alloc(npages, spc, needzero)
11751176
if s == nil {
11761177
throw("out of memory")
11771178
}
1179+
if go115NewMCentralImpl {
1180+
// Put the large span in the mcentral swept list so that it's
1181+
// visible to the background sweeper.
1182+
mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
1183+
}
11781184
s.limit = s.base() + size
11791185
heapBitsForAddr(s.base()).initSpan(s)
11801186
return s

src/runtime/mcache.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ func (c *mcache) refill(spc spanClass) {
131131
if s.sweepgen != mheap_.sweepgen+3 {
132132
throw("bad sweepgen in refill")
133133
}
134-
atomic.Store(&s.sweepgen, mheap_.sweepgen)
134+
if go115NewMCentralImpl {
135+
mheap_.central[spc].mcentral.uncacheSpan(s)
136+
} else {
137+
atomic.Store(&s.sweepgen, mheap_.sweepgen)
138+
}
135139
}
136140

137141
// Get a new cached span from the central lists.

src/runtime/mcentral.go

Lines changed: 238 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,31 @@ import "runtime/internal/atomic"
2020
type mcentral struct {
2121
lock mutex
2222
spanclass spanClass
23-
nonempty mSpanList // list of spans with a free object, ie a nonempty free list
24-
empty mSpanList // list of spans with no free objects (or cached in an mcache)
23+
24+
// For !go115NewMCentralImpl.
25+
nonempty mSpanList // list of spans with a free object, ie a nonempty free list
26+
empty mSpanList // list of spans with no free objects (or cached in an mcache)
27+
28+
// partial and full contain two mspan sets: one of swept in-use
29+
// spans, and one of unswept in-use spans. These two trade
30+
// roles on each GC cycle. The unswept set is drained either by
31+
// allocation or by the background sweeper in every GC cycle,
32+
// so only two roles are necessary.
33+
//
34+
// sweepgen is increased by 2 on each GC cycle, so the swept
35+
// spans are in partial[sweepgen/2%2] and the unswept spans are in
36+
// partial[1-sweepgen/2%2]. Sweeping pops spans from the
37+
// unswept set and pushes spans that are still in-use on the
38+
// swept set. Likewise, allocating an in-use span pushes it
39+
// on the swept set.
40+
//
41+
// Some parts of the sweeper can sweep arbitrary spans, and hence
42+
// can't remove them from the unswept set, but will add the span
43+
// to the appropriate swept list. As a result, the parts of the
44+
// sweeper and mcentral that do consume from the unswept list may
45+
// encounter swept spans, and these should be ignored.
46+
partial [2]spanSet // list of spans with a free object
47+
full [2]spanSet // list of spans with no free objects
2548

2649
// nmalloc is the cumulative count of objects allocated from
2750
// this mcentral, assuming all spans in mcaches are
@@ -32,13 +55,151 @@ type mcentral struct {
3255
// Initialize a single central free list.
3356
func (c *mcentral) init(spc spanClass) {
3457
c.spanclass = spc
35-
c.nonempty.init()
36-
c.empty.init()
37-
lockInit(&c.lock, lockRankMcentral)
58+
if go115NewMCentralImpl {
59+
lockInit(&c.partial[0].spineLock, lockRankSpanSetSpine)
60+
lockInit(&c.partial[1].spineLock, lockRankSpanSetSpine)
61+
lockInit(&c.full[0].spineLock, lockRankSpanSetSpine)
62+
lockInit(&c.full[1].spineLock, lockRankSpanSetSpine)
63+
} else {
64+
c.nonempty.init()
65+
c.empty.init()
66+
lockInit(&c.lock, lockRankMcentral)
67+
}
68+
}
69+
70+
// partialUnswept returns the spanSet which holds partially-filled
71+
// unswept spans for this sweepgen.
72+
func (c *mcentral) partialUnswept(sweepgen uint32) *spanSet {
73+
return &c.partial[1-sweepgen/2%2]
74+
}
75+
76+
// partialSwept returns the spanSet which holds partially-filled
77+
// swept spans for this sweepgen.
78+
func (c *mcentral) partialSwept(sweepgen uint32) *spanSet {
79+
return &c.partial[sweepgen/2%2]
80+
}
81+
82+
// fullUnswept returns the spanSet which holds unswept spans without any
83+
// free slots for this sweepgen.
84+
func (c *mcentral) fullUnswept(sweepgen uint32) *spanSet {
85+
return &c.full[1-sweepgen/2%2]
86+
}
87+
88+
// fullSwept returns the spanSet which holds swept spans without any
89+
// free slots for this sweepgen.
90+
func (c *mcentral) fullSwept(sweepgen uint32) *spanSet {
91+
return &c.full[sweepgen/2%2]
3892
}
3993

4094
// Allocate a span to use in an mcache.
4195
func (c *mcentral) cacheSpan() *mspan {
96+
if !go115NewMCentralImpl {
97+
return c.oldCacheSpan()
98+
}
99+
// Deduct credit for this span allocation and sweep if necessary.
100+
spanBytes := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) * _PageSize
101+
deductSweepCredit(spanBytes, 0)
102+
103+
sg := mheap_.sweepgen
104+
105+
traceDone := false
106+
if trace.enabled {
107+
traceGCSweepStart()
108+
}
109+
var s *mspan
110+
111+
// Try partial swept spans first.
112+
if s = c.partialSwept(sg).pop(); s != nil {
113+
goto havespan
114+
}
115+
// Now try partial unswept spans.
116+
for {
117+
s = c.partialUnswept(sg).pop()
118+
if s == nil {
119+
break
120+
}
121+
if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
122+
// We got ownership of the span, so let's sweep it and use it.
123+
s.sweep(true)
124+
goto havespan
125+
}
126+
// We failed to get ownership of the span, which means it's being or
127+
// has been swept by an asynchronous sweeper that just couldn't remove it
128+
// from the unswept list. That sweeper took ownership of the span and
129+
// responsibility for either freeing it to the heap or putting it on the
130+
// right swept list. Either way, we should just ignore it (and it's unsafe
131+
// for us to do anything else).
132+
}
133+
// Now try full unswept spans, sweeping them and putting them into the
134+
// right list if we fail to get a span.
135+
for {
136+
s = c.fullUnswept(sg).pop()
137+
if s == nil {
138+
break
139+
}
140+
if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
141+
// We got ownership of the span, so let's sweep it.
142+
s.sweep(true)
143+
// Check if there's any free space.
144+
freeIndex := s.nextFreeIndex()
145+
if freeIndex != s.nelems {
146+
s.freeindex = freeIndex
147+
goto havespan
148+
}
149+
// Add it to the swept list, because sweeping didn't give us any free space.
150+
c.fullSwept(sg).push(s)
151+
}
152+
// See comment for partial unswept spans.
153+
}
154+
if trace.enabled {
155+
traceGCSweepDone()
156+
traceDone = true
157+
}
158+
159+
// We failed to get a span from the mcentral so get one from mheap.
160+
s = c.grow()
161+
if s == nil {
162+
return nil
163+
}
164+
165+
// At this point s is a span that should have free slots.
166+
havespan:
167+
if trace.enabled && !traceDone {
168+
traceGCSweepDone()
169+
}
170+
n := int(s.nelems) - int(s.allocCount)
171+
if n == 0 || s.freeindex == s.nelems || uintptr(s.allocCount) == s.nelems {
172+
throw("span has no free objects")
173+
}
174+
// Assume all objects from this span will be allocated in the
175+
// mcache. If it gets uncached, we'll adjust this.
176+
atomic.Xadd64(&c.nmalloc, int64(n))
177+
usedBytes := uintptr(s.allocCount) * s.elemsize
178+
atomic.Xadd64(&memstats.heap_live, int64(spanBytes)-int64(usedBytes))
179+
if trace.enabled {
180+
// heap_live changed.
181+
traceHeapAlloc()
182+
}
183+
if gcBlackenEnabled != 0 {
184+
// heap_live changed.
185+
gcController.revise()
186+
}
187+
freeByteBase := s.freeindex &^ (64 - 1)
188+
whichByte := freeByteBase / 8
189+
// Init alloc bits cache.
190+
s.refillAllocCache(whichByte)
191+
192+
// Adjust the allocCache so that s.freeindex corresponds to the low bit in
193+
// s.allocCache.
194+
s.allocCache >>= s.freeindex % 64
195+
196+
return s
197+
}
198+
199+
// Allocate a span to use in an mcache.
200+
//
201+
// For !go115NewMCentralImpl.
202+
func (c *mcentral) oldCacheSpan() *mspan {
42203
// Deduct credit for this span allocation and sweep if necessary.
43204
spanBytes := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) * _PageSize
44205
deductSweepCredit(spanBytes, 0)
@@ -148,7 +309,77 @@ havespan:
148309
}
149310

150311
// Return span from an mcache.
312+
//
313+
// s must have a span class corresponding to this
314+
// mcentral and it must not be empty.
151315
func (c *mcentral) uncacheSpan(s *mspan) {
316+
if !go115NewMCentralImpl {
317+
c.oldUncacheSpan(s)
318+
return
319+
}
320+
if s.allocCount == 0 {
321+
throw("uncaching span but s.allocCount == 0")
322+
}
323+
324+
sg := mheap_.sweepgen
325+
stale := s.sweepgen == sg+1
326+
327+
// Fix up sweepgen.
328+
if stale {
329+
// Span was cached before sweep began. It's our
330+
// responsibility to sweep it.
331+
//
332+
// Set sweepgen to indicate it's not cached but needs
333+
// sweeping and can't be allocated from. sweep will
334+
// set s.sweepgen to indicate s is swept.
335+
atomic.Store(&s.sweepgen, sg-1)
336+
} else {
337+
// Indicate that s is no longer cached.
338+
atomic.Store(&s.sweepgen, sg)
339+
}
340+
n := int(s.nelems) - int(s.allocCount)
341+
342+
// Fix up statistics.
343+
if n > 0 {
344+
// cacheSpan updated alloc assuming all objects on s
345+
// were going to be allocated. Adjust for any that
346+
// weren't. We must do this before potentially
347+
// sweeping the span.
348+
atomic.Xadd64(&c.nmalloc, -int64(n))
349+
350+
if !stale {
351+
// (*mcentral).cacheSpan conservatively counted
352+
// unallocated slots in heap_live. Undo this.
353+
//
354+
// If this span was cached before sweep, then
355+
// heap_live was totally recomputed since
356+
// caching this span, so we don't do this for
357+
// stale spans.
358+
atomic.Xadd64(&memstats.heap_live, -int64(n)*int64(s.elemsize))
359+
}
360+
}
361+
362+
// Put the span in the appropriate place.
363+
if stale {
364+
// It's stale, so just sweep it. Sweeping will put it on
365+
// the right list.
366+
s.sweep(false)
367+
} else {
368+
if n > 0 {
369+
// Put it back on the partial swept list.
370+
c.partialSwept(sg).push(s)
371+
} else {
372+
// There's no free space and it's not stale, so put it on the
373+
// full swept list.
374+
c.fullSwept(sg).push(s)
375+
}
376+
}
377+
}
378+
379+
// Return span from an mcache.
380+
//
381+
// For !go115NewMCentralImpl.
382+
func (c *mcentral) oldUncacheSpan(s *mspan) {
152383
if s.allocCount == 0 {
153384
throw("uncaching span but s.allocCount == 0")
154385
}
@@ -207,6 +438,8 @@ func (c *mcentral) uncacheSpan(s *mspan) {
207438
// freeSpan reports whether s was returned to the heap.
208439
// If preserve=true, it does not move s (the caller
209440
// must take care of it).
441+
//
442+
// For !go115NewMCentralImpl.
210443
func (c *mcentral) freeSpan(s *mspan, preserve bool, wasempty bool) bool {
211444
if sg := mheap_.sweepgen; s.sweepgen == sg+1 || s.sweepgen == sg+3 {
212445
throw("freeSpan given cached span")

src/runtime/mgc.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,7 @@ func gcStart(trigger gcTrigger) {
13201320
systemstack(func() {
13211321
finishsweep_m()
13221322
})
1323+
13231324
// clearpools before we start the GC. If we wait they memory will not be
13241325
// reclaimed until the next GC cycle.
13251326
clearpools()
@@ -2141,6 +2142,9 @@ func gcMark(start_time int64) {
21412142

21422143
// gcSweep must be called on the system stack because it acquires the heap
21432144
// lock. See mheap for details.
2145+
//
2146+
// The world must be stopped.
2147+
//
21442148
//go:systemstack
21452149
func gcSweep(mode gcMode) {
21462150
if gcphase != _GCoff {
@@ -2150,7 +2154,7 @@ func gcSweep(mode gcMode) {
21502154
lock(&mheap_.lock)
21512155
mheap_.sweepgen += 2
21522156
mheap_.sweepdone = 0
2153-
if mheap_.sweepSpans[mheap_.sweepgen/2%2].index != 0 {
2157+
if !go115NewMCentralImpl && mheap_.sweepSpans[mheap_.sweepgen/2%2].index != 0 {
21542158
// We should have drained this list during the last
21552159
// sweep phase. We certainly need to start this phase
21562160
// with an empty swept list.
@@ -2162,6 +2166,10 @@ func gcSweep(mode gcMode) {
21622166
mheap_.reclaimCredit = 0
21632167
unlock(&mheap_.lock)
21642168

2169+
if go115NewMCentralImpl {
2170+
sweep.centralIndex.clear()
2171+
}
2172+
21652173
if !_ConcurrentSweep || mode == gcForceBlockMode {
21662174
// Special case synchronous sweep.
21672175
// Record that no proportional sweeping has to happen.

0 commit comments

Comments
 (0)