Skip to content

Commit eacdf76

Browse files
committed
runtime: add bitmap-based markrootSpans implementation
Currently markrootSpans, the scanning routine which scans span specials (particularly finalizers) as roots, uses sweepSpans to shard work and find spans to mark. However, as part of a future CL to change span ownership and how mcentral works, we want to avoid having markrootSpans use the sweep bufs to find specials, so in this change we introduce a new mechanism. Much like for the page reclaimer, we set up a per-page bitmap where the first page for a span is marked if the span contains any specials, and unmarked if it has no specials. This bitmap is updated by addspecial, removespecial, and during sweeping. markrootSpans then shards this bitmap into mark work and markers iterate over the bitmap looking for spans with specials to mark. Unlike the page reclaimer, we don't need to use the pageInUse bits because having a special implies that a span is in-use. While in terms of computational complexity this design is technically worse, because it needs to iterate over the mapped heap, in practice this iteration is very fast (we can skip over large swathes of the heap very quickly) and we only look at spans that have any specials at all, rather than having to touch each span. This new implementation of markrootSpans is behind a feature flag called go115NewMarkrootSpans. Updates #37487. Change-Id: I8ea07b6c11059f6d412fe419e0ab512d989377b8 Reviewed-on: https://go-review.googlesource.com/c/go/+/221178 Run-TryBot: Michael Knyszek <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Austin Clements <[email protected]>
1 parent 2a2423b commit eacdf76

File tree

4 files changed

+193
-34
lines changed

4 files changed

+193
-34
lines changed

src/runtime/malloc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,14 @@ func mallocinit() {
465465
physHugePageShift++
466466
}
467467
}
468+
if pagesPerArena%pagesPerSpanRoot != 0 {
469+
print("pagesPerArena (", pagesPerArena, ") is not divisible by pagesPerSpanRoot (", pagesPerSpanRoot, ")\n")
470+
throw("bad pagesPerSpanRoot")
471+
}
472+
if pagesPerArena%pagesPerReclaimerChunk != 0 {
473+
print("pagesPerArena (", pagesPerArena, ") is not divisible by pagesPerReclaimerChunk (", pagesPerReclaimerChunk, ")\n")
474+
throw("bad pagesPerReclaimerChunk")
475+
}
468476

469477
// Initialize the heap.
470478
mheap_.init()

src/runtime/mgcmark.go

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ const (
2121
// BSS root.
2222
rootBlockBytes = 256 << 10
2323

24-
// rootBlockSpans is the number of spans to scan per span
25-
// root.
26-
rootBlockSpans = 8 * 1024 // 64MB worth of spans
27-
2824
// maxObletBytes is the maximum bytes of an object to scan at
2925
// once. Larger objects will be split up into "oblets" of at
3026
// most this size. Since we can scan 1–2 MB/ms, 128 KB bounds
@@ -41,14 +37,26 @@ const (
4137
// a syscall, so its overhead is nontrivial). Higher values
4238
// make the system less responsive to incoming work.
4339
drainCheckThreshold = 100000
40+
41+
// pagesPerSpanRoot indicates how many pages to scan from a span root
42+
// at a time. Used by special root marking.
43+
//
44+
// Higher values improve throughput by increasing locality, but
45+
// increase the minimum latency of a marking operation.
46+
//
47+
// Must be a multiple of the pageInUse bitmap element size and
48+
// must also evenly divide pagesPerArena.
49+
pagesPerSpanRoot = 512
50+
51+
// go115NewMarkrootSpans is a feature flag that indicates whether
52+
// to use the new bitmap-based markrootSpans implementation.
53+
go115NewMarkrootSpans = true
4454
)
4555

4656
// gcMarkRootPrepare queues root scanning jobs (stacks, globals, and
4757
// some miscellany) and initializes scanning-related state.
4858
//
4959
// The world must be stopped.
50-
//
51-
//go:nowritebarrier
5260
func gcMarkRootPrepare() {
5361
work.nFlushCacheRoots = 0
5462

@@ -79,13 +87,24 @@ func gcMarkRootPrepare() {
7987
//
8088
// We depend on addfinalizer to mark objects that get
8189
// finalizers after root marking.
82-
//
83-
// We're only interested in scanning the in-use spans,
84-
// which will all be swept at this point. More spans
85-
// may be added to this list during concurrent GC, but
86-
// we only care about spans that were allocated before
87-
// this mark phase.
88-
work.nSpanRoots = mheap_.sweepSpans[mheap_.sweepgen/2%2].numBlocks()
90+
if go115NewMarkrootSpans {
91+
// We're going to scan the whole heap (that was available at the time the
92+
// mark phase started, i.e. markArenas) for in-use spans which have specials.
93+
//
94+
// Break up the work into arenas, and further into chunks.
95+
//
96+
// Snapshot allArenas as markArenas. This snapshot is safe because allArenas
97+
// is append-only.
98+
mheap_.markArenas = mheap_.allArenas[:len(mheap_.allArenas):len(mheap_.allArenas)]
99+
work.nSpanRoots = len(mheap_.markArenas) * (pagesPerArena / pagesPerSpanRoot)
100+
} else {
101+
// We're only interested in scanning the in-use spans,
102+
// which will all be swept at this point. More spans
103+
// may be added to this list during concurrent GC, but
104+
// we only care about spans that were allocated before
105+
// this mark phase.
106+
work.nSpanRoots = mheap_.sweepSpans[mheap_.sweepgen/2%2].numBlocks()
107+
}
89108

90109
// Scan stacks.
91110
//
@@ -293,10 +312,96 @@ func markrootFreeGStacks() {
293312
unlock(&sched.gFree.lock)
294313
}
295314

296-
// markrootSpans marks roots for one shard of work.spans.
315+
// markrootSpans marks roots for one shard of markArenas.
297316
//
298317
//go:nowritebarrier
299318
func markrootSpans(gcw *gcWork, shard int) {
319+
if !go115NewMarkrootSpans {
320+
oldMarkrootSpans(gcw, shard)
321+
return
322+
}
323+
// Objects with finalizers have two GC-related invariants:
324+
//
325+
// 1) Everything reachable from the object must be marked.
326+
// This ensures that when we pass the object to its finalizer,
327+
// everything the finalizer can reach will be retained.
328+
//
329+
// 2) Finalizer specials (which are not in the garbage
330+
// collected heap) are roots. In practice, this means the fn
331+
// field must be scanned.
332+
sg := mheap_.sweepgen
333+
334+
// Find the arena and page index into that arena for this shard.
335+
ai := mheap_.markArenas[shard/(pagesPerArena/pagesPerSpanRoot)]
336+
ha := mheap_.arenas[ai.l1()][ai.l2()]
337+
arenaPage := uint(uintptr(shard) * pagesPerSpanRoot % pagesPerArena)
338+
339+
// Construct slice of bitmap which we'll iterate over.
340+
specialsbits := ha.pageSpecials[arenaPage/8:]
341+
specialsbits = specialsbits[:pagesPerSpanRoot/8]
342+
for i := range specialsbits {
343+
// Find set bits, which correspond to spans with specials.
344+
specials := atomic.Load8(&specialsbits[i])
345+
if specials == 0 {
346+
continue
347+
}
348+
for j := uint(0); j < 8; j++ {
349+
if specials&(1<<j) == 0 {
350+
continue
351+
}
352+
// Find the span for this bit.
353+
//
354+
// This value is guaranteed to be non-nil because having
355+
// specials implies that the span is in-use, and since we're
356+
// currently marking we can be sure that we don't have to worry
357+
// about the span being freed and re-used.
358+
s := ha.spans[arenaPage+uint(i)*8+j]
359+
360+
// The state must be mSpanInUse if the specials bit is set, so
361+
// sanity check that.
362+
if state := s.state.get(); state != mSpanInUse {
363+
print("s.state = ", state, "\n")
364+
throw("non in-use span found with specials bit set")
365+
}
366+
// Check that this span was swept (it may be cached or uncached).
367+
if !useCheckmark && !(s.sweepgen == sg || s.sweepgen == sg+3) {
368+
// sweepgen was updated (+2) during non-checkmark GC pass
369+
print("sweep ", s.sweepgen, " ", sg, "\n")
370+
throw("gc: unswept span")
371+
}
372+
373+
// Lock the specials to prevent a special from being
374+
// removed from the list while we're traversing it.
375+
lock(&s.speciallock)
376+
for sp := s.specials; sp != nil; sp = sp.next {
377+
if sp.kind != _KindSpecialFinalizer {
378+
continue
379+
}
380+
// don't mark finalized object, but scan it so we
381+
// retain everything it points to.
382+
spf := (*specialfinalizer)(unsafe.Pointer(sp))
383+
// A finalizer can be set for an inner byte of an object, find object beginning.
384+
p := s.base() + uintptr(spf.special.offset)/s.elemsize*s.elemsize
385+
386+
// Mark everything that can be reached from
387+
// the object (but *not* the object itself or
388+
// we'll never collect it).
389+
scanobject(p, gcw)
390+
391+
// The special itself is a root.
392+
scanblock(uintptr(unsafe.Pointer(&spf.fn)), sys.PtrSize, &oneptrmask[0], gcw, nil)
393+
}
394+
unlock(&s.speciallock)
395+
}
396+
}
397+
}
398+
399+
// oldMarkrootSpans marks roots for one shard of work.spans.
400+
//
401+
// For go115NewMarkrootSpans = false.
402+
//
403+
//go:nowritebarrier
404+
func oldMarkrootSpans(gcw *gcWork, shard int) {
300405
// Objects with finalizers have two GC-related invariants:
301406
//
302407
// 1) Everything reachable from the object must be marked.

src/runtime/mgcsweep.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func (s *mspan) sweep(preserve bool) bool {
246246
// 2. A tiny object can have several finalizers setup for different offsets.
247247
// If such object is not marked, we need to queue all finalizers at once.
248248
// Both 1 and 2 are possible at the same time.
249+
hadSpecials := s.specials != nil
249250
specialp := &s.specials
250251
special := *specialp
251252
for special != nil {
@@ -290,6 +291,9 @@ func (s *mspan) sweep(preserve bool) bool {
290291
special = *specialp
291292
}
292293
}
294+
if go115NewMarkrootSpans && hadSpecials && s.specials == nil {
295+
spanHasNoSpecials(s)
296+
}
293297

294298
if debug.allocfreetrace != 0 || debug.clobberfree != 0 || raceenabled || msanenabled {
295299
// Find all newly freed objects. This doesn't have to

src/runtime/mheap.go

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ const (
2727
// maxPhysHugePageSize sets an upper-bound on the maximum huge page size
2828
// that the runtime supports.
2929
maxPhysHugePageSize = pallocChunkBytes
30+
31+
// pagesPerReclaimerChunk indicates how many pages to scan from the
32+
// pageInUse bitmap at a time. Used by the page reclaimer.
33+
//
34+
// Higher values reduce contention on scanning indexes (such as
35+
// h.reclaimIndex), but increase the minimum latency of the
36+
// operation.
37+
//
38+
// The time required to scan this many pages can vary a lot depending
39+
// on how many spans are actually freed. Experimentally, it can
40+
// scan for pages at ~300 GB/ms on a 2.6GHz Core i7, but can only
41+
// free spans at ~32 MB/ms. Using 512 pages bounds this at
42+
// roughly 100µs.
43+
//
44+
// Must be a multiple of the pageInUse bitmap element size and
45+
// must also evenly divid pagesPerArena.
46+
pagesPerReclaimerChunk = 512
3047
)
3148

3249
// Main malloc heap.
@@ -180,13 +197,19 @@ type mheap struct {
180197
// simply blocking GC (by disabling preemption).
181198
sweepArenas []arenaIdx
182199

200+
// markArenas is a snapshot of allArenas taken at the beginning
201+
// of the mark cycle. Because allArenas is append-only, neither
202+
// this slice nor its contents will change during the mark, so
203+
// it can be read safely.
204+
markArenas []arenaIdx
205+
183206
// curArena is the arena that the heap is currently growing
184207
// into. This should always be physPageSize-aligned.
185208
curArena struct {
186209
base, end uintptr
187210
}
188211

189-
_ uint32 // ensure 64-bit alignment of central
212+
// _ uint32 // ensure 64-bit alignment of central
190213

191214
// central free lists for small size classes.
192215
// the padding makes sure that the mcentrals are
@@ -256,6 +279,16 @@ type heapArena struct {
256279
// operations.
257280
pageMarks [pagesPerArena / 8]uint8
258281

282+
// pageSpecials is a bitmap that indicates which spans have
283+
// specials (finalizers or other). Like pageInUse, only the bit
284+
// corresponding to the first page in each span is used.
285+
//
286+
// Writes are done atomically whenever a special is added to
287+
// a span and whenever the last special is removed from a span.
288+
// Reads are done atomically to find spans containing specials
289+
// during marking.
290+
pageSpecials [pagesPerArena / 8]uint8
291+
259292
// zeroedBase marks the first byte of the first page in this
260293
// arena which hasn't been used yet and is therefore already
261294
// zero. zeroedBase is relative to the arena base.
@@ -706,23 +739,10 @@ func (h *mheap) init() {
706739
//
707740
// h must NOT be locked.
708741
func (h *mheap) reclaim(npage uintptr) {
709-
// This scans pagesPerChunk at a time. Higher values reduce
710-
// contention on h.reclaimPos, but increase the minimum
711-
// latency of performing a reclaim.
712-
//
713-
// Must be a multiple of the pageInUse bitmap element size.
714-
//
715-
// The time required by this can vary a lot depending on how
716-
// many spans are actually freed. Experimentally, it can scan
717-
// for pages at ~300 GB/ms on a 2.6GHz Core i7, but can only
718-
// free spans at ~32 MB/ms. Using 512 pages bounds this at
719-
// roughly 100µs.
720-
//
721742
// TODO(austin): Half of the time spent freeing spans is in
722743
// locking/unlocking the heap (even with low contention). We
723744
// could make the slow path here several times faster by
724745
// batching heap frees.
725-
const pagesPerChunk = 512
726746

727747
// Bail early if there's no more reclaim work.
728748
if atomic.Load64(&h.reclaimIndex) >= 1<<63 {
@@ -755,7 +775,7 @@ func (h *mheap) reclaim(npage uintptr) {
755775
}
756776

757777
// Claim a chunk of work.
758-
idx := uintptr(atomic.Xadd64(&h.reclaimIndex, pagesPerChunk) - pagesPerChunk)
778+
idx := uintptr(atomic.Xadd64(&h.reclaimIndex, pagesPerReclaimerChunk) - pagesPerReclaimerChunk)
759779
if idx/pagesPerArena >= uintptr(len(arenas)) {
760780
// Page reclaiming is done.
761781
atomic.Store64(&h.reclaimIndex, 1<<63)
@@ -769,7 +789,7 @@ func (h *mheap) reclaim(npage uintptr) {
769789
}
770790

771791
// Scan this chunk.
772-
nfound := h.reclaimChunk(arenas, idx, pagesPerChunk)
792+
nfound := h.reclaimChunk(arenas, idx, pagesPerReclaimerChunk)
773793
if nfound <= npage {
774794
npage -= nfound
775795
} else {
@@ -1593,6 +1613,22 @@ type special struct {
15931613
kind byte // kind of special
15941614
}
15951615

1616+
// spanHasSpecials marks a span as having specials in the arena bitmap.
1617+
func spanHasSpecials(s *mspan) {
1618+
arenaPage := (s.base() / pageSize) % pagesPerArena
1619+
ai := arenaIndex(s.base())
1620+
ha := mheap_.arenas[ai.l1()][ai.l2()]
1621+
atomic.Or8(&ha.pageSpecials[arenaPage/8], uint8(1)<<(arenaPage%8))
1622+
}
1623+
1624+
// spanHasNoSpecials marks a span as having no specials in the arena bitmap.
1625+
func spanHasNoSpecials(s *mspan) {
1626+
arenaPage := (s.base() / pageSize) % pagesPerArena
1627+
ai := arenaIndex(s.base())
1628+
ha := mheap_.arenas[ai.l1()][ai.l2()]
1629+
atomic.And8(&ha.pageSpecials[arenaPage/8], ^(uint8(1) << (arenaPage % 8)))
1630+
}
1631+
15961632
// Adds the special record s to the list of special records for
15971633
// the object p. All fields of s should be filled in except for
15981634
// offset & next, which this routine will fill in.
@@ -1638,6 +1674,9 @@ func addspecial(p unsafe.Pointer, s *special) bool {
16381674
s.offset = uint16(offset)
16391675
s.next = *t
16401676
*t = s
1677+
if go115NewMarkrootSpans {
1678+
spanHasSpecials(span)
1679+
}
16411680
unlock(&span.speciallock)
16421681
releasem(mp)
16431682

@@ -1661,6 +1700,7 @@ func removespecial(p unsafe.Pointer, kind uint8) *special {
16611700

16621701
offset := uintptr(p) - span.base()
16631702

1703+
var result *special
16641704
lock(&span.speciallock)
16651705
t := &span.specials
16661706
for {
@@ -1672,15 +1712,17 @@ func removespecial(p unsafe.Pointer, kind uint8) *special {
16721712
// "interior" specials (p must be exactly equal to s->offset).
16731713
if offset == uintptr(s.offset) && kind == s.kind {
16741714
*t = s.next
1675-
unlock(&span.speciallock)
1676-
releasem(mp)
1677-
return s
1715+
result = s
1716+
break
16781717
}
16791718
t = &s.next
16801719
}
1720+
if go115NewMarkrootSpans && span.specials == nil {
1721+
spanHasNoSpecials(span)
1722+
}
16811723
unlock(&span.speciallock)
16821724
releasem(mp)
1683-
return nil
1725+
return result
16841726
}
16851727

16861728
// The described object has a finalizer set for it.

0 commit comments

Comments
 (0)