mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
This CL is part of a set of CLs that attempt to reduce how much work the GC must do. See the design in https://go.dev/design/74299-runtime-freegc This CL adds runtime.freegc: func freegc(ptr unsafe.Pointer, uintptr size, noscan bool) Memory freed via runtime.freegc is made immediately reusable for the next allocation in the same size class, without waiting for a GC cycle, and hence can dramatically reduce pressure on the GC. A sample microbenchmark included below shows strings.Builder operating roughly 2x faster. An experimental modification to reflect to use runtime.freegc and then using that reflect with json/v2 gave reported memory allocation reductions of -43.7%, -32.9%, -21.9%, -22.0%, -1.0% for the 5 official real-world unmarshalling benchmarks from go-json-experiment/jsonbench by the authors of json/v2, covering the CanadaGeometry through TwitterStatus datasets. Note: there is no intent to modify the standard library to have explicit calls to runtime.freegc, and of course such an ability would never be exposed to end-user code. Later CLs in this stack teach the compiler how to automatically insert runtime.freegc calls when it can prove it is safe to do so. (The reflect modification and other experimental changes to the standard library were just that -- experiments. It was very helpful while initially developing runtime.freegc to see more complex uses and closer-to-real-world benchmark results prior to updating the compiler.) This CL only addresses noscan span classes (heap objects without pointers), such as the backing memory for a []byte or string. A follow-on CL adds support for heap objects with pointers. If we update strings.Builder to explicitly call runtime.freegc on its internal buf after a resize operation (but without freeing the usually final incarnation of buf that will be returned to the user as a string), we can see some nice benchmark results on the existing strings benchmarks that call Builder.Write N times and then call Builder.String. Here, the (uncommon) case of a single Builder.Write is not helped (given it never resizes after first alloc if there is only one Write), but the impact grows such that it is up to ~2x faster as there are more resize operations due to more strings.Builder.Write calls: │ disabled.out │ new-free-20.txt │ │ sec/op │ sec/op vs base │ BuildString_Builder/1Write_36Bytes_NoGrow-4 55.82n ± 2% 55.86n ± 2% ~ (p=0.794 n=20) BuildString_Builder/2Write_36Bytes_NoGrow-4 125.2n ± 2% 115.4n ± 1% -7.86% (p=0.000 n=20) BuildString_Builder/3Write_36Bytes_NoGrow-4 224.0n ± 1% 188.2n ± 2% -16.00% (p=0.000 n=20) BuildString_Builder/5Write_36Bytes_NoGrow-4 239.1n ± 9% 205.1n ± 1% -14.20% (p=0.000 n=20) BuildString_Builder/8Write_36Bytes_NoGrow-4 422.8n ± 3% 325.4n ± 1% -23.04% (p=0.000 n=20) BuildString_Builder/10Write_36Bytes_NoGrow-4 436.9n ± 2% 342.3n ± 1% -21.64% (p=0.000 n=20) BuildString_Builder/100Write_36Bytes_NoGrow-4 4.403µ ± 1% 2.381µ ± 2% -45.91% (p=0.000 n=20) BuildString_Builder/1000Write_36Bytes_NoGrow-4 48.28µ ± 2% 21.38µ ± 2% -55.71% (p=0.000 n=20) See the design document for more discussion of the strings.Builder case. For testing, we add tests that attempt to exercise different aspects of the underlying freegc and mallocgc behavior on the reuse path. Validating the assist credit manipulations turned out to be subtle, so a test for that is added in the next CL. There are also invariant checks added, controlled by consts (primarily the doubleCheckReusable const currently). This CL also adds support in runtime.freegc for GODEBUG=clobberfree=1 to immediately overwrite freed memory with 0xdeadbeef, which can help a higher-level test fail faster in the event of a bug, and also the GC specifically looks for that pattern and throws a fatal error if it unexpectedly finds it. A later CL (currently experimental) adds GODEBUG=clobberfree=2, which uses mprotect (or VirtualProtect on Windows) to set freed memory to fault if read or written, until the runtime later unprotects the memory on the mallocgc reuse path. For the cases where a normal allocation is happening without any reuse, some initial microbenchmarks suggest the impact of these changes could be small to negligible (at least with GOAMD64=v3): goos: linux goarch: amd64 pkg: runtime cpu: AMD EPYC 7B13 │ base-512M-v3.bench │ ps16-512M-goamd64-v3.bench │ │ sec/op │ sec/op vs base │ Malloc8-16 11.01n ± 1% 10.94n ± 1% -0.68% (p=0.038 n=20) Malloc16-16 17.15n ± 1% 17.05n ± 0% -0.55% (p=0.007 n=20) Malloc32-16 18.65n ± 1% 18.42n ± 0% -1.26% (p=0.000 n=20) MallocTypeInfo8-16 18.63n ± 0% 18.36n ± 0% -1.45% (p=0.000 n=20) MallocTypeInfo16-16 22.32n ± 0% 22.65n ± 0% +1.50% (p=0.000 n=20) MallocTypeInfo32-16 23.37n ± 0% 23.89n ± 0% +2.23% (p=0.000 n=20) geomean 18.02n 18.01n -0.05% These last benchmark results include the runtime updates to support span classes with pointers (which was originally part of this CL, but later split out for ease of review). Updates #74299 Change-Id: Icceaa0f79f85c70cd1a718f9a4e7f0cf3d77803c Reviewed-on: https://go-review.googlesource.com/c/go/+/673695 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Knyszek <mknyszek@google.com> Reviewed-by: Junyang Shao <shaojunyang@google.com>
391 lines
12 KiB
Go
391 lines
12 KiB
Go
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package runtime
|
|
|
|
import (
|
|
"internal/runtime/atomic"
|
|
"internal/runtime/gc"
|
|
"internal/runtime/sys"
|
|
"unsafe"
|
|
)
|
|
|
|
// Per-thread (in Go, per-P) cache for small objects.
|
|
// This includes a small object cache and local allocation stats.
|
|
// No locking needed because it is per-thread (per-P).
|
|
//
|
|
// mcaches are allocated from non-GC'd memory, so any heap pointers
|
|
// must be specially handled.
|
|
type mcache struct {
|
|
_ sys.NotInHeap
|
|
|
|
// The following members are accessed on every malloc,
|
|
// so they are grouped here for better caching.
|
|
nextSample int64 // trigger heap sample after allocating this many bytes
|
|
memProfRate int // cached mem profile rate, used to detect changes
|
|
scanAlloc uintptr // bytes of scannable heap allocated
|
|
|
|
// Allocator cache for tiny objects w/o pointers.
|
|
// See "Tiny allocator" comment in malloc.go.
|
|
|
|
// tiny points to the beginning of the current tiny block, or
|
|
// nil if there is no current tiny block.
|
|
//
|
|
// tiny is a heap pointer. Since mcache is in non-GC'd memory,
|
|
// we handle it by clearing it in releaseAll during mark
|
|
// termination.
|
|
//
|
|
// tinyAllocs is the number of tiny allocations performed
|
|
// by the P that owns this mcache.
|
|
tiny uintptr
|
|
tinyoffset uintptr
|
|
tinyAllocs uintptr
|
|
|
|
// The rest is not accessed on every malloc.
|
|
|
|
// alloc contains spans to allocate from, indexed by spanClass.
|
|
alloc [numSpanClasses]*mspan
|
|
|
|
// TODO(thepudds): better to interleave alloc and reusableScan/reusableNoscan so that
|
|
// a single malloc call can often access both in the same cache line for a given spanClass.
|
|
// It's not interleaved right now in part to have slightly smaller diff, and might be
|
|
// negligible effect on current microbenchmarks.
|
|
|
|
// reusableNoscan contains linked lists of reusable noscan heap objects, indexed by spanClass.
|
|
// The next pointers are stored in the first word of the heap objects.
|
|
reusableNoscan [numSpanClasses]gclinkptr
|
|
|
|
stackcache [_NumStackOrders]stackfreelist
|
|
|
|
// flushGen indicates the sweepgen during which this mcache
|
|
// was last flushed. If flushGen != mheap_.sweepgen, the spans
|
|
// in this mcache are stale and need to be flushed so they
|
|
// can be swept. This is done in acquirep.
|
|
flushGen atomic.Uint32
|
|
}
|
|
|
|
// A gclink is a node in a linked list of blocks, like mlink,
|
|
// but it is opaque to the garbage collector.
|
|
// The GC does not trace the pointers during collection,
|
|
// and the compiler does not emit write barriers for assignments
|
|
// of gclinkptr values. Code should store references to gclinks
|
|
// as gclinkptr, not as *gclink.
|
|
type gclink struct {
|
|
next gclinkptr
|
|
}
|
|
|
|
// A gclinkptr is a pointer to a gclink, but it is opaque
|
|
// to the garbage collector.
|
|
type gclinkptr uintptr
|
|
|
|
// ptr returns the *gclink form of p.
|
|
// The result should be used for accessing fields, not stored
|
|
// in other data structures.
|
|
func (p gclinkptr) ptr() *gclink {
|
|
return (*gclink)(unsafe.Pointer(p))
|
|
}
|
|
|
|
type stackfreelist struct {
|
|
list gclinkptr // linked list of free stacks
|
|
size uintptr // total size of stacks in list
|
|
}
|
|
|
|
// dummy mspan that contains no free objects.
|
|
var emptymspan mspan
|
|
|
|
func allocmcache() *mcache {
|
|
var c *mcache
|
|
systemstack(func() {
|
|
lock(&mheap_.lock)
|
|
c = (*mcache)(mheap_.cachealloc.alloc())
|
|
c.flushGen.Store(mheap_.sweepgen)
|
|
unlock(&mheap_.lock)
|
|
})
|
|
for i := range c.alloc {
|
|
c.alloc[i] = &emptymspan
|
|
}
|
|
c.nextSample = nextSample()
|
|
|
|
return c
|
|
}
|
|
|
|
// freemcache releases resources associated with this
|
|
// mcache and puts the object onto a free list.
|
|
//
|
|
// In some cases there is no way to simply release
|
|
// resources, such as statistics, so donate them to
|
|
// a different mcache (the recipient).
|
|
func freemcache(c *mcache) {
|
|
systemstack(func() {
|
|
c.releaseAll()
|
|
stackcache_clear(c)
|
|
|
|
// NOTE(rsc,rlh): If gcworkbuffree comes back, we need to coordinate
|
|
// with the stealing of gcworkbufs during garbage collection to avoid
|
|
// a race where the workbuf is double-freed.
|
|
// gcworkbuffree(c.gcworkbuf)
|
|
|
|
lock(&mheap_.lock)
|
|
mheap_.cachealloc.free(unsafe.Pointer(c))
|
|
unlock(&mheap_.lock)
|
|
})
|
|
}
|
|
|
|
// getMCache is a convenience function which tries to obtain an mcache.
|
|
//
|
|
// Returns nil if we're not bootstrapping or we don't have a P. The caller's
|
|
// P must not change, so we must be in a non-preemptible state.
|
|
func getMCache(mp *m) *mcache {
|
|
// Grab the mcache, since that's where stats live.
|
|
pp := mp.p.ptr()
|
|
var c *mcache
|
|
if pp == nil {
|
|
// We will be called without a P while bootstrapping,
|
|
// in which case we use mcache0, which is set in mallocinit.
|
|
// mcache0 is cleared when bootstrapping is complete,
|
|
// by procresize.
|
|
c = mcache0
|
|
} else {
|
|
c = pp.mcache
|
|
}
|
|
return c
|
|
}
|
|
|
|
// refill acquires a new span of span class spc for c. This span will
|
|
// have at least one free object. The current span in c must be full.
|
|
//
|
|
// Must run in a non-preemptible context since otherwise the owner of
|
|
// c could change.
|
|
func (c *mcache) refill(spc spanClass) {
|
|
// Return the current cached span to the central lists.
|
|
s := c.alloc[spc]
|
|
|
|
if s.allocCount != s.nelems {
|
|
throw("refill of span with free space remaining")
|
|
}
|
|
|
|
// TODO(thepudds): we might be able to allow mallocgcTiny to reuse 16 byte objects from spc==5,
|
|
// but for now, just clear our reusable objects for tinySpanClass.
|
|
if spc == tinySpanClass {
|
|
c.reusableNoscan[spc] = 0
|
|
}
|
|
if c.reusableNoscan[spc] != 0 {
|
|
throw("refill of span with reusable pointers remaining on pointer free list")
|
|
}
|
|
|
|
if s != &emptymspan {
|
|
// Mark this span as no longer cached.
|
|
if s.sweepgen != mheap_.sweepgen+3 {
|
|
throw("bad sweepgen in refill")
|
|
}
|
|
mheap_.central[spc].mcentral.uncacheSpan(s)
|
|
|
|
// Count up how many slots were used and record it.
|
|
stats := memstats.heapStats.acquire()
|
|
slotsUsed := int64(s.allocCount) - int64(s.allocCountBeforeCache)
|
|
atomic.Xadd64(&stats.smallAllocCount[spc.sizeclass()], slotsUsed)
|
|
|
|
// Flush tinyAllocs.
|
|
if spc == tinySpanClass {
|
|
atomic.Xadd64(&stats.tinyAllocCount, int64(c.tinyAllocs))
|
|
c.tinyAllocs = 0
|
|
}
|
|
memstats.heapStats.release()
|
|
|
|
// Count the allocs in inconsistent, internal stats.
|
|
bytesAllocated := slotsUsed * int64(s.elemsize)
|
|
gcController.totalAlloc.Add(bytesAllocated)
|
|
|
|
// Clear the second allocCount just to be safe.
|
|
s.allocCountBeforeCache = 0
|
|
}
|
|
|
|
// Get a new cached span from the central lists.
|
|
s = mheap_.central[spc].mcentral.cacheSpan()
|
|
if s == nil {
|
|
throw("out of memory")
|
|
}
|
|
|
|
if s.allocCount == s.nelems {
|
|
throw("span has no free space")
|
|
}
|
|
|
|
// Indicate that this span is cached and prevent asynchronous
|
|
// sweeping in the next sweep phase.
|
|
s.sweepgen = mheap_.sweepgen + 3
|
|
|
|
// Store the current alloc count for accounting later.
|
|
s.allocCountBeforeCache = s.allocCount
|
|
|
|
// Update heapLive and flush scanAlloc.
|
|
//
|
|
// We have not yet allocated anything new into the span, but we
|
|
// assume that all of its slots will get used, so this makes
|
|
// heapLive an overestimate.
|
|
//
|
|
// When the span gets uncached, we'll fix up this overestimate
|
|
// if necessary (see releaseAll).
|
|
//
|
|
// We pick an overestimate here because an underestimate leads
|
|
// the pacer to believe that it's in better shape than it is,
|
|
// which appears to lead to more memory used. See #53738 for
|
|
// more details.
|
|
usedBytes := uintptr(s.allocCount) * s.elemsize
|
|
gcController.update(int64(s.npages*pageSize)-int64(usedBytes), int64(c.scanAlloc))
|
|
c.scanAlloc = 0
|
|
|
|
c.alloc[spc] = s
|
|
}
|
|
|
|
// allocLarge allocates a span for a large object.
|
|
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
|
|
if size+pageSize < size {
|
|
throw("out of memory")
|
|
}
|
|
npages := size >> gc.PageShift
|
|
if size&pageMask != 0 {
|
|
npages++
|
|
}
|
|
|
|
// Deduct credit for this span allocation and sweep if
|
|
// necessary. mHeap_Alloc will also sweep npages, so this only
|
|
// pays the debt down to npage pages.
|
|
deductSweepCredit(npages*pageSize, npages)
|
|
|
|
spc := makeSpanClass(0, noscan)
|
|
s := mheap_.alloc(npages, spc)
|
|
if s == nil {
|
|
throw("out of memory")
|
|
}
|
|
|
|
// Count the alloc in consistent, external stats.
|
|
stats := memstats.heapStats.acquire()
|
|
atomic.Xadd64(&stats.largeAlloc, int64(npages*pageSize))
|
|
atomic.Xadd64(&stats.largeAllocCount, 1)
|
|
memstats.heapStats.release()
|
|
|
|
// Count the alloc in inconsistent, internal stats.
|
|
gcController.totalAlloc.Add(int64(npages * pageSize))
|
|
|
|
// Update heapLive.
|
|
gcController.update(int64(s.npages*pageSize), 0)
|
|
|
|
// Put the large span in the mcentral swept list so that it's
|
|
// visible to the background sweeper.
|
|
mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
|
|
|
|
// Adjust s.limit down to the object-containing part of the span.
|
|
//
|
|
// This is just to create a slightly tighter bound on the limit.
|
|
// It's totally OK if the garbage collector, in particular
|
|
// conservative scanning, can temporarily observes an inflated
|
|
// limit. It will simply mark the whole object or just skip it
|
|
// since we're in the mark phase anyway.
|
|
s.limit = s.base() + size
|
|
s.initHeapBits()
|
|
return s
|
|
}
|
|
|
|
func (c *mcache) releaseAll() {
|
|
// Take this opportunity to flush scanAlloc.
|
|
scanAlloc := int64(c.scanAlloc)
|
|
c.scanAlloc = 0
|
|
|
|
sg := mheap_.sweepgen
|
|
dHeapLive := int64(0)
|
|
for i := range c.alloc {
|
|
s := c.alloc[i]
|
|
if s != &emptymspan {
|
|
slotsUsed := int64(s.allocCount) - int64(s.allocCountBeforeCache)
|
|
s.allocCountBeforeCache = 0
|
|
|
|
// Adjust smallAllocCount for whatever was allocated.
|
|
stats := memstats.heapStats.acquire()
|
|
atomic.Xadd64(&stats.smallAllocCount[spanClass(i).sizeclass()], slotsUsed)
|
|
memstats.heapStats.release()
|
|
|
|
// Adjust the actual allocs in inconsistent, internal stats.
|
|
// We assumed earlier that the full span gets allocated.
|
|
gcController.totalAlloc.Add(slotsUsed * int64(s.elemsize))
|
|
|
|
if s.sweepgen != sg+1 {
|
|
// refill conservatively counted unallocated slots in gcController.heapLive.
|
|
// Undo this.
|
|
//
|
|
// If this span was cached before sweep, then gcController.heapLive was totally
|
|
// recomputed since caching this span, so we don't do this for stale spans.
|
|
dHeapLive -= int64(s.nelems-s.allocCount) * int64(s.elemsize)
|
|
}
|
|
|
|
// Release the span to the mcentral.
|
|
mheap_.central[i].mcentral.uncacheSpan(s)
|
|
c.alloc[i] = &emptymspan
|
|
}
|
|
}
|
|
// Clear tinyalloc pool.
|
|
c.tiny = 0
|
|
c.tinyoffset = 0
|
|
|
|
// Flush tinyAllocs.
|
|
stats := memstats.heapStats.acquire()
|
|
atomic.Xadd64(&stats.tinyAllocCount, int64(c.tinyAllocs))
|
|
c.tinyAllocs = 0
|
|
memstats.heapStats.release()
|
|
|
|
// Clear the reusable linked lists.
|
|
// For noscan objects, the nodes of the linked lists are the reusable heap objects themselves,
|
|
// so we can simply clear the linked list head pointers.
|
|
// TODO(thepudds): consider having debug logging of a non-empty reusable lists getting cleared,
|
|
// maybe based on the existing debugReusableLog.
|
|
clear(c.reusableNoscan[:])
|
|
|
|
// Update heapLive and heapScan.
|
|
gcController.update(dHeapLive, scanAlloc)
|
|
}
|
|
|
|
// prepareForSweep flushes c if the system has entered a new sweep phase
|
|
// since c was populated. This must happen between the sweep phase
|
|
// starting and the first allocation from c.
|
|
func (c *mcache) prepareForSweep() {
|
|
// Alternatively, instead of making sure we do this on every P
|
|
// between starting the world and allocating on that P, we
|
|
// could leave allocate-black on, allow allocation to continue
|
|
// as usual, use a ragged barrier at the beginning of sweep to
|
|
// ensure all cached spans are swept, and then disable
|
|
// allocate-black. However, with this approach it's difficult
|
|
// to avoid spilling mark bits into the *next* GC cycle.
|
|
sg := mheap_.sweepgen
|
|
flushGen := c.flushGen.Load()
|
|
if flushGen == sg {
|
|
return
|
|
} else if flushGen != sg-2 {
|
|
println("bad flushGen", flushGen, "in prepareForSweep; sweepgen", sg)
|
|
throw("bad flushGen")
|
|
}
|
|
c.releaseAll()
|
|
stackcache_clear(c)
|
|
c.flushGen.Store(mheap_.sweepgen) // Synchronizes with gcStart
|
|
}
|
|
|
|
// addReusableNoscan adds a noscan object pointer to the reusable pointer free list
|
|
// for a span class.
|
|
func (c *mcache) addReusableNoscan(spc spanClass, ptr uintptr) {
|
|
if !runtimeFreegcEnabled {
|
|
return
|
|
}
|
|
|
|
// Add to the reusable pointers free list.
|
|
v := gclinkptr(ptr)
|
|
v.ptr().next = c.reusableNoscan[spc]
|
|
c.reusableNoscan[spc] = v
|
|
}
|
|
|
|
// hasReusableNoscan reports whether there is a reusable object available for
|
|
// a noscan spc.
|
|
func (c *mcache) hasReusableNoscan(spc spanClass) bool {
|
|
if !runtimeFreegcEnabled {
|
|
return false
|
|
}
|
|
return c.reusableNoscan[spc] != 0
|
|
}
|