Skip to content

Commit 051809e

Browse files
committed
runtime: free workbufs during sweeping
This extends the sweeper to free workbufs back to the heap between GC cycles, allowing this memory to be reused for GC'd allocations or eventually returned to the OS. This helps for applications that have high peak heap usage relative to their regular heap usage (for example, a high-memory initialization phase). Workbuf memory is roughly proportional to heap size and since we currently never free workbufs, it's proportional to *peak* heap size. By freeing workbufs, we can release and reuse this memory for other purposes when the heap shrinks. This is somewhat complicated because this costs ~1–2 µs per workbuf span, so for large heaps it's too expensive to just do synchronously after mark termination between starting the world and dropping the worldsema. Hence, we do it asynchronously in the sweeper. This adds a list of "free" workbuf spans that can be returned to the heap. GC moves all workbuf spans to this list after mark termination and the background sweeper drains this list back to the heap. If the sweeper doesn't finish, that's fine, since getempty can directly reuse any remaining spans to allocate more workbufs. Performance impact is negligible. On the x/benchmarks, this reduces GC-bytes-from-system by 6–11%. Fixes #19325. Change-Id: Icb92da2196f0c39ee984faf92d52f29fd9ded7a8 Reviewed-on: https://go-review.googlesource.com/38582 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rick Hudson <[email protected]>
1 parent 9cc883a commit 051809e

File tree

4 files changed

+99
-8
lines changed

4 files changed

+99
-8
lines changed

src/runtime/mgc.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,9 @@ var work struct {
797797

798798
wbufSpans struct {
799799
lock mutex
800+
// free is a list of spans dedicated to workbufs, but
801+
// that don't currently contain any workbufs.
802+
free mSpanList
800803
// busy is a list of all spans containing workbufs on
801804
// one of the workbuf lists.
802805
busy mSpanList
@@ -1480,6 +1483,10 @@ func gcMarkTermination() {
14801483
// world stopped.
14811484
mProf_Flush()
14821485

1486+
// Prepare workbufs for freeing by the sweeper. We do this
1487+
// asynchronously because it can take non-trivial time.
1488+
prepareFreeWorkbufs()
1489+
14831490
// Free stack spans. This must be done between GC cycles.
14841491
systemstack(freeStackSpans)
14851492

@@ -1923,6 +1930,10 @@ func gcSweep(mode gcMode) {
19231930
for sweepone() != ^uintptr(0) {
19241931
sweep.npausesweep++
19251932
}
1933+
// Free workbufs eagerly.
1934+
prepareFreeWorkbufs()
1935+
for freeSomeWbufs(false) {
1936+
}
19261937
// All "free" events for this mark/sweep cycle have
19271938
// now happened, so we can make this profile cycle
19281939
// available immediately.

src/runtime/mgcsweep.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ func bgsweep(c chan int) {
5656
sweep.nbgsweep++
5757
Gosched()
5858
}
59+
for freeSomeWbufs(true) {
60+
Gosched()
61+
}
5962
lock(&sweep.lock)
6063
if !gosweepdone() {
6164
// This can happen if a GC runs between

src/runtime/mgcwork.go

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -334,16 +334,27 @@ func getempty() *workbuf {
334334
if b == nil {
335335
// Allocate more workbufs.
336336
var s *mspan
337-
systemstack(func() {
338-
s = mheap_.allocManual(workbufAlloc/pageSize, &memstats.gc_sys)
339-
})
337+
if work.wbufSpans.free.first != nil {
338+
lock(&work.wbufSpans.lock)
339+
s = work.wbufSpans.free.first
340+
if s != nil {
341+
work.wbufSpans.free.remove(s)
342+
work.wbufSpans.busy.insert(s)
343+
}
344+
unlock(&work.wbufSpans.lock)
345+
}
340346
if s == nil {
341-
throw("out of memory")
347+
systemstack(func() {
348+
s = mheap_.allocManual(workbufAlloc/pageSize, &memstats.gc_sys)
349+
})
350+
if s == nil {
351+
throw("out of memory")
352+
}
353+
// Record the new span in the busy list.
354+
lock(&work.wbufSpans.lock)
355+
work.wbufSpans.busy.insert(s)
356+
unlock(&work.wbufSpans.lock)
342357
}
343-
// Record the new span in the busy list.
344-
lock(&work.wbufSpans.lock)
345-
work.wbufSpans.busy.insert(s)
346-
unlock(&work.wbufSpans.lock)
347358
// Slice up the span into new workbufs. Return one and
348359
// put the rest on the empty list.
349360
for i := uintptr(0); i+_WorkbufSize <= workbufAlloc; i += _WorkbufSize {
@@ -456,3 +467,44 @@ func handoff(b *workbuf) *workbuf {
456467
putfull(b)
457468
return b1
458469
}
470+
471+
// prepareFreeWorkbufs moves busy workbuf spans to free list so they
472+
// can be freed to the heap. This must only be called when all
473+
// workbufs are on the empty list.
474+
func prepareFreeWorkbufs() {
475+
lock(&work.wbufSpans.lock)
476+
if work.full != 0 {
477+
throw("cannot free workbufs when work.full != 0")
478+
}
479+
// Since all workbufs are on the empty list, we don't care
480+
// which ones are in which spans. We can wipe the entire empty
481+
// list and move all workbuf spans to the free list.
482+
work.empty = 0
483+
work.wbufSpans.free.takeAll(&work.wbufSpans.busy)
484+
unlock(&work.wbufSpans.lock)
485+
}
486+
487+
// freeSomeWbufs frees some workbufs back to the heap and returns
488+
// true if it should be called again to free more.
489+
func freeSomeWbufs(preemptible bool) bool {
490+
const batchSize = 64 // ~1–2 µs per span.
491+
lock(&work.wbufSpans.lock)
492+
if gcphase != _GCoff || work.wbufSpans.free.isEmpty() {
493+
unlock(&work.wbufSpans.lock)
494+
return false
495+
}
496+
systemstack(func() {
497+
gp := getg().m.curg
498+
for i := 0; i < batchSize && !(preemptible && gp.preempt); i++ {
499+
span := work.wbufSpans.free.first
500+
if span == nil {
501+
break
502+
}
503+
work.wbufSpans.free.remove(span)
504+
mheap_.freeManual(span, &memstats.gc_sys)
505+
}
506+
})
507+
more := !work.wbufSpans.free.isEmpty()
508+
unlock(&work.wbufSpans.lock)
509+
return more
510+
}

src/runtime/mheap.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,31 @@ func (list *mSpanList) insertBack(span *mspan) {
12041204
span.list = list
12051205
}
12061206

1207+
// takeAll removes all spans from other and inserts them at the front
1208+
// of list.
1209+
func (list *mSpanList) takeAll(other *mSpanList) {
1210+
if other.isEmpty() {
1211+
return
1212+
}
1213+
1214+
// Reparent everything in other to list.
1215+
for s := other.first; s != nil; s = s.next {
1216+
s.list = list
1217+
}
1218+
1219+
// Concatenate the lists.
1220+
if list.isEmpty() {
1221+
*list = *other
1222+
} else {
1223+
// Neither list is empty. Put other before list.
1224+
other.last.next = list.first
1225+
list.first.prev = other.last
1226+
list.first = other.first
1227+
}
1228+
1229+
other.first, other.last = nil, nil
1230+
}
1231+
12071232
const (
12081233
_KindSpecialFinalizer = 1
12091234
_KindSpecialProfile = 2

0 commit comments

Comments
 (0)