Memory management
Thread map, Go 1.0 – 1.17
| Thread | 1.0 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 | 1.7 | 1.8 | 1.9 | 1.10 | 1.11 | 1.12 | 1.13 | 1.14 | 1.15 | 1.16 | 1.17 | Status at go1.17 (or at the end of its era slice) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Memory management: GC and allocator (Go 1.0–1.17) | |||||||||||||||||||
| Stop-the-world mark-sweep, then the parallel collector | ● | ● | ● | ● | ● | replaced by the concurrent collector (threads "Concurrent mark" and "Decentralized GC state machine"); the parallel work distribution (parfor, work buffers) lives on as the root-job / gcWork machinery | |||||||||||||
| Concurrent sweep and proportional sweep | ● | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (background bgsweep plus proportional on-demand sweeping and the page reclaimer; sweepone driven by heap_live) → continues as Sweeper termination and pacing (1.18+) | |||||||||
| Concurrent mark: write barriers and the dev.garbage collector | ● | ● | ● | still the structure of the collector (concurrent mark with write barriers, 25% utilisation goal, assists); the barrier flavour changed in 1.8 (hybrid barrier) → continues as Sweeper termination and pacing and Mark worker scheduling and GC work queues (1.18+) | |||||||||||||||
| Decentralized GC state machine and mark termination simplification | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (gcStart/gcMarkDone/gcMarkTermination run by whichever goroutine notices the transition; checkmarks mode independent of STW code) → continues as Mark worker scheduling and GC work queues (1.18+) | ||||||||||
| GC pacer: trigger controller, assists and background utilisation | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (trigger-ratio feedback controller, scan-work based assists, 25% background utilisation, soft/hard goal) but now isolated in mgcpacer.go as gcControllerState; → continues as GC pacer redesign (1.18+) | ||||||||
| Stack scanning: conservative, precise by frames, stack barriers, hybrid barrier, async safe points | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | stack roots scanned precisely using stack maps and stack objects, with no rescan, plus conservative scanning at async safe points; → continues as Stack and register root scanning (1.18+) | |||||||
| Hybrid write barrier and buffered write barrier | ● | ● | ● | ● | ● | still the design at go1.17 (deletion+insertion "hybrid" barrier with a per-P pointer buffer flushed through wbBufFlush) → continues as Stack and register root scanning (1.18+) | |||||||||||||
| Precise heap scanning: type information, the heap bitmap and GC programs | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (2-bit/1-bit-per-word sparse per-arena heap bitmap written by heapBitsSetType, GC programs for large types, mark bits in separate per-span bitmaps) → continues as Heap bitmap rewrite and allocation headers (1.18+) | ||||||
| Allocating from mark bits: allocBits, freeindex and ctz | ● | ● | ● | ● | still the design at go1.17 (per-span allocBits/gcmarkBits, freeindex, 64-bit allocCache, nextFreeFast) → continues as Allocation publication, zeroing and preemptibility (1.18+) | ||||||||||||||
| Size classes, tiny allocator and mcache/mcentral | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (_NumSizeClasses = 68 entries at go1.17 including the 24-byte class, spanClass splitting scan/noscan, mcentral built on spanSet, mcache holds one span per span class) → continues as mallocgc decomposition and size-specialized malloc (1.24+) | |||||
| Span lifecycle in mheap: free lists, treaps, first-fit and the page reclaimer | ● | ● | ● | ● | ● | ● | ● | ● | ● | replaced by the bitmap page allocator in 1.14 (see "The bitmap page allocator"); the in-use span bookkeeping (spanSet, page reclaimer, in-use bitmap) is still the design at go1.17 | |||||||||
| The bitmap page allocator | ● | ● | ● | ● | still the design at go1.17 (radix-tree summaries over per-chunk 4 MiB allocation bitmaps, searchAddr, sparse chunk index, inUse address ranges) → continues as Scavenger redesign and Address-space layout, tagged pointers and the mem.go layer (1.18+) | ||||||||||||||
| Scavenger: returning memory to the OS | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | a background scavenger paced by its own cost measurements over the page allocator's address-ordered search with a "free"/"scavenged" watermark; → continues as Scavenger redesign (1.18+) | |||||||
| Heap address space: contiguous arena to sparse arenas | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (64 MB heapArena units on 64-bit, 4 MB on Windows and iOS, two-level arena map on platforms with large address spaces, arenaBaseOffset, heap-arena hints) → continues as Address-space layout, tagged pointers and the mem.go layer (1.18+) | |||||
| Finalizers and span specials | ● | ● | ● | ● | ● | ● | ● | ● | ● | finalizers as special records on spans, scanned as roots concurrently, run by one goroutine; → continues as Finalizers, runtime.AddCleanup and the cleanup queue (1.19+) | |||||||||
| Mark work distribution: parfor, work buffers, gcWork and root jobs | ● | ● | ● | ● | ● | ● | ● | ● | ● | still the design at go1.17 (per-P gcWork with two cached work buffers, global full/empty lists, markroot jobs for data/BSS/span/stack roots shared by workers and assists) → continues as Mark worker scheduling and GC work queues (1.19+) | |||||||||
| Runtime allocators for runtime metadata and the port to Go | ● | ● | ● | ● | ● | persistentalloc, fixalloc and manually-managed spans are still the metadata allocators; the allocator and collector are Go code since 1.5 | |||||||||||||
| Memory statistics and accounting | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● | sharded consistentHeapStats and per-mcache stats feed MemStats, GC pacer state lives in gcController | |||||||
| Allocation-site profiling and GC debugging modes | ● | ● | ● | ● | ● | ● | ● | gccheckmark, clobberfree, zombie-slot detection, scavtrace, allocfreetrace, invalidptr and cgocheck are all present | |||||||||||
Thread map, Go 1.18 – 1.28
| Thread | 1.18 | 1.19 | 1.20 | 1.21 | 1.22 | 1.23 | 1.24 | 1.25 | 1.26 | 1.27 | 1.28 | Status at HEAD |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Runtime: memory allocator and garbage collector (Go 1.18–1.28) | ||||||||||||
| GC pacer redesign | ● | ● | ● | ● | redesigned pacer is the only pacer since 1.19 (PacerRedesign GOEXPERIMENT added off in 1.18, defaulted on in 1.18, removed in 1.19); the 512 KiB heap minimum it introduced still sits behind the off-by-default HeapMinimum512KiB experiment | |||||||
| Soft memory limit and GC CPU limiter | ● | ● | ● | ● | ● | GOMEMLIMIT/debug.SetMemoryLimit and the always-on GC CPU limiter shipped in 1.19; the feature flag was removed in 1.21; accounting fixes ran through 1.25 | ||||||
| Green Tea: span-based marking with vector scan kernels | ● | ● | ● | ● | GOEXPERIMENT=greenteagc added off in 1.25, default on in 1.26 (loong64 included later in 1.26); flag and the mgcmark_nogreenteagc.go fallback still present; AVX-512 kernels only on amd64 | |||||||
| Heap bitmap rewrite and allocation headers | ● | ● | ● | ● | ● | ● | allocation headers are the only heap-metadata scheme (experiment added and defaulted in 1.22, removed in 1.23); GC programs removed in 1.24 | |||||
| mallocgc decomposition and size-specialized malloc | ● | ● | ● | ● | ● | sizespecializedmalloc added and defaulted on in the 1.26 cycle, off for the go1.26.0 release, on again in 1.27, flag removed in 1.28; specialization now covers size classes 1–7 (≤80 B); unconditional except on plan9/race/asan/msan/valgrind | ||||||
| Scavenger redesign | ● | ● | ● | ● | ● | ● | controller-paced background scavenger at 1% of total CPU, chunk-index based search, memory-limit and GOGC goals | |||||
| Transparent huge page management | ● | ● | explicit heap THP marking removed in 1.22; density-based scavenger deferral and metadata huge-page toggling remain; GODEBUG=disablethp available | |||||||||
| Finalizers, runtime.AddCleanup and the cleanup queue | ● | ● | ● | ● | ● | ● | ● | ● | AddCleanup shipped in 1.24; cleanups run on up to GOMAXPROCS goroutines since 1.25; GODEBUG=checkfinalizer leak detector since 1.25; misuse panics since 1.26 | |||
| Weak pointers and the unique package | ● | ● | ● | ● | ● | unique since 1.23, public weak since 1.24; unique uses a bespoke weak canonicalization map since 1.25 | ||||||
| Object pinning (runtime.Pinner) | ● | ● | ● | ● | ● | runtime.Pinner shipped in 1.21; 1.28 cut pin/unpin costs with batching and per-P caches | ||||||
| Memory arenas experiment | ● | ● | ● | ● | GOEXPERIMENT=arenas (package arena, reflect.ArenaNew) present and off by default since 1.20; no further feature work | |||||||
| runtime.freegc: compiler-directed explicit freeing | ● | GOEXPERIMENT=runtimefreegc present, off by default; noscan objects only | ||||||||||
| Mark worker scheduling and GC work queues | ● | ● | ● | ● | ● | ● | idle workers capped and suppressed when dedicated workers exist (1.19); worker selection integrated into start-the-world (1.26) | |||||
| Allocation publication, zeroing and preemptibility | ● | ● | ● | ● | ● | ● | allocate-black plus freeIndexForScan snapshot; largeType as atomic publication point; preemptible large clears still pending | |||||
| Sweeper termination and pacing | ● | ● | ● | ● | ● | ● | ● | single-atomic sweep termination; bgsweep yields only when no Ps are idle | ||||
| Stack and register root scanning | ● | ● | ● | ● | ● | allgs snapshot at mark start; async-preemption register state (scalar, vector, LR frame) scanned conservatively | ||||||
| Address-space layout, tagged pointers and the mem.go layer | ● | ● | ● | ● | ● | ● | ● | ● | ● | per-platform heapAddrBits with 9 low tag bits; all OS mapping goes through mem.go state transitions | ||
| Linear-memory heaps (Wasm and sbrk platforms) | ● | ● | ● | ● | ● | Wasm and Plan 9 share the sbrk free-block allocator; Wasm uses 512 KiB arenas | ||||||
| Heap base address randomization | ● | ● | GOEXPERIMENT=randomizedheapbase64 on by default since 1.26 (off under race/asan/msan) | |||||||||
Memory management: GC and allocator (Go 1.0–1.17)
Reader: Claude Sonnet. Cites 1,151 of 1,367 commits in this slice; every non-noise commit is cited, plus 19 larger commits the noise filter had set aside, which a separate gap reader described. Read the design-heavy commit messages (very long ones truncated) and followed up on selected commits; 54 trivial build, portability and style commits are cited as one grouped item.
Release digest for this slice
- Go 1.0 — Everything up to the go1 tag: tcmalloc-style allocator (2008-12), stop-the-world mark-sweep (2009-01), finalizers, a faster collector in 2011 (
1e063b32cd), helper threads, memory release to the OS (5c598d3c9f); threads “Stop-the-world mark-sweep…”, “Size classes…”, “Scavenger”, “Finalizers”. - Go 1.1 — Parallel GC with
parforand per-thread work buffers, and the first precise heap scanning from type information; “Stop-the-world mark-sweep…”, “Precise heap scanning”, “Mark work distribution”. - Go 1.2 — Allocator refinement for per-P caches (whole-span transfer, inlined malloc fast path),
persistentalloc, lazy page table and flexible 64-bit mapping; precise stack scan tried and reverted. - Go 1.3 — Concurrent sweep (
3c3be62201), 8K pages, tiny allocator, specials replacing the finalizer table, parallel stack scanning. - Go 1.4 — Allocator and collector prepared for Go:
freeremoved, stack allocator split out, byte-array bitmap, more parallelism; GC still stop-the-world. - Go 1.5 — The concurrent collector: allocator and GC translated to Go, dev.garbage barriers, pacer with assists and utilisation scheduling, 2-bit heap bitmap, stack barriers; the largest release for the GC arc.
- Go 1.6 — Pause work: root jobs in workers, STW loops removed, decentralised state machine, background GC goroutine removed.
- Go 1.7 — dev.garbage bitmap allocator (allocBits, freeindex, ctz), allocate-black, O(dirty) stack rescan, concurrent stack shrink, 32-bit full address space.
- Go 1.8 — Hybrid write barrier ends STW stack rescans; bigger static size classes; sweep and root marking proportional to in-use spans;
NextGCredefined. - Go 1.9 — Noscan spans and
spanClass, concurrentruntime.GC, treap for large spans, rationalised trigger, workbufs as manual spans. - Go 1.10 — Soft/hard heap goals and 5% assist headroom, buffered write barrier, worker scheduling cleanup.
- Go 1.11 — Sparse heap: arena index, sparse bitmap and span map, no heap size limit.
- Go 1.12 — Mark termination simplification (mark 2, gchelper, gctrace=2 removed), page reclaimer, treap-only span tracking, aggressive scavenger with fixes.
- Go 1.13 — Background scavenger, first-fit address-ordered allocation, huge-page-aware scavenging, effective GOGC.
- Go 1.14 — New bitmap page allocator, self-paced scavenger, async-preemption support for GC (conservative scanning), trigger ratio floor.
- Go 1.15 — Lock-free
spanSet-based mcentral, offset-address ranges, zombie-slot detection, scavenger watermarks, preemptible dedicated mark workers. - Go 1.16 — Consistent sharded heap statistics, 24-byte size class, checkmarks bitmap, bit-parallel summaries.
- Go 1.17 — Pacer extracted into
mgcpacer.goasgcControllerState, sweep completion unified, incremental arena preparation, stack-object GC data only.
Stop-the-world mark-sweep, then the parallel collector
Span: Go 1.0 → Go 1.4 · Status at go1.17: replaced by the concurrent collector (threads “Concurrent mark” and “Decentralized GC state machine”); the parallel work distribution (parfor, work buffers) lives on as the root-job / gcWork machinery
- design doc Simpler and faster GC for Go listed in Go: A Documentary
- design doc Better GC and Memory Allocator for Go listed in Go: A Documentary
- discussion Better GC and Memory Allocator for Go golang-dev · listed in Go: A Documentary
- design doc Go 1.4+ GC Plan and Roadmap listed in Go: A Documentary
Design. The collector began as gc #0 (1ce17918e3, 2009-01): a single-threaded, stop-the-world mark-and-sweep in mgc0.c over the tcmalloc-derived allocator that had landed a month earlier (e29ce175ed, “malloc in runtime, not used by default”). Marking found block starts through the heap map and a per-block bitmap; sweep walked an explicit allspan list (9f726c2c8b) and returned dead objects to size-class free lists. Early tuning is about making the collector cheaper per object: sweepspan0/sweepspan1 specialisation and inlined free (fb94be55dc), no zeroing of blocks on free (991a968f44), GC clocked on bytes allocated instead of pages in use so fragmentation cannot delay a collection (8ddd6c4181).
The 2011 rewrite 1e063b32cd (“faster allocator, garbage collector”, pauses about half as long, still single-threaded) preceded the first parallel collector: helper threads limited by ncpu (a5f064a3e1), a leak fix in the parallel collector (8219cc9af8), and then in 1.1 the real parallelisation by Dmitry Vyukov: per-M GC stats (d839a809b2), MHeap.allspans as an array so a “parallel for” can partition it (342658bbb6), the parfor algorithm (95643647ae), batch free in sweep (4945fc8e40, 845aa1fc2c), faster mark (b0702bd0db, MaxGcproc 4 to 8; an accidental 16 had been reverted by eb0bc8164a), prefetching in mark (f09e63a2a0), and per-thread work buffers instead of one mutex-protected pool (d4c80d19a8). In 1.4 the same machinery got more threads (MaxGcprocs to 32, 44106a0880, citing parallel roots/stacks, parallel stack shrinking 192bccbf33, per-thread workbuf caches f6f2f77142), non-atomic mark bit updates (0c32bd6262; also when GOMAXPROCS=1, c6fe53a230), and cheaper scan loops (b8fdaaf028, 9dc6764d3c). Deleting code was repeatedly the optimisation: a6d8b483b6 (5-20% faster, suggested by Sanjay Ghemawat) and 03f2189a1b (remove backward bitmap scanning).
Timeline.
- Go 1.0 — Done:
- 2008-2009: allocator and first collector (
e29ce175ed,1ce17918e3,9f726c2c8b); heap map split from malloc for 64-bit (80f4ab47ee); a bug in the size-class waste check that produced far too many classes fixed (c1e748bd2e); stacks no longer malloc’d during GC (8c357ce269). - 2010: finalizers merged into package runtime (
33e396a4a7); sweep and GC specialisation (f25586a306,fb94be55dc,22a7f2a14ddrops MHeapMapCache); GC clocked on bytes allocated (8ddd6c4181); memory profiling hook (596c16e045). - 2011: simpler dense heap map in bss (
4608feb18b); faster allocator and collector (1e063b32cd); standard-sized stacks off the central FixAlloc (c9152a8568); helper threads for parallel marking (a5f064a3e1,8219cc9af8); per-M MemStats caches (66d5c9b1e9,27753ff108). - 2012:
ReadMemStatsreplaces UpdateMemStats (842c906e2e); 32-bit fallback when the 64-bit reservation fails (d37a8b73c5). Left open: GC was stop-the-world and single-threaded in the go1 tag; stack and heap scanning were conservative.
- 2008-2009: allocator and first collector (
- Go 1.1 — Done:
parfor-based parallel GC (d839a809b2,342658bbb6,95643647ae,b0702bd0db,d4c80d19a8,2924638d26replaces a lock withcasp); sweep batch free (4945fc8e40); zeroing moved out of the heap mutex and skipped for huge blocks (c1c851bbe8); GC stats (cea46387b9) and the stop-the-world time shown separately in GOGCTRACE (fd32ac4bae); user-visibleruntime/debugGC controls (472354f81e). Left open: marking depends on conservative scanning of stacks and untyped blocks (see precise-GC thread). - Go 1.2 — Done: the collector runs on g0 instead of a goroutine stack (
71f061043d,de316388a7), and may not be triggered from g0 (dfdd1ba028); GC and the finalizer goroutine are synchronised for the preemptive scheduler (72c4ee1a9d);setGCPercentrace fixed (94dc963b55). Left open: nothing noted beyond stack precision. - Go 1.3 — Done: stack scanning moved into the parallel mark phase (
0368a7ceb6); free-list objects kept marked-as-allocated so malloc/free need not touch bits (2d20c0d625, later restored after the GC rewrite as187d0f67201.4); data race in GC fixed (b3a3afc9b7);GODEBUG=gcdead/allocfreetraceadjusted (1ec4d5e9e7); 3x fewer nanotime calls (95ee7d6414); markspan 2x on a NUMA box (acb03b8028); freeOSMemory now sweeps (e893acf184). Left open: sweep still inside the stop-the-world pause until the concurrent sweep thread. - Go 1.4 — Done: parallelism pass (
44106a0880,192bccbf33,f6f2f77142,0c32bd6262,98bebcc90aparfor in Go,aac7f1a0d6markallocated in Go,c46bcd4d13finalizer goroutine in Go); stack shrinking delayed until GC is done to avoid the GC seeing freed stacks throughSudoG.elem(91e8554b8b); invalid pointers into the GC arena crash (11e53e46a6);MemStats.PauseEnd(46af78915a); go1.4gctraceshows goroutine count (ed7db89b90). Left open: all of it still stop-the-world, with mark termination doing all marking; the power64 port disabled GC (9b88857f751.5, in the dev.power64 branch landed in 1.5).
Concurrent sweep and proportional sweep
Span: Go 1.3 → Go 1.17 · Status at go1.17: still the design at go1.17 (background bgsweep plus proportional on-demand sweeping and the page reclaimer; sweepone driven by heap_live) → continues as Sweeper termination and pacing (1.18+)
Paths not taken: Asynchronous sweeping for small allocations (never landed)
- motivating issue #18155 runtime: latency in sweep assists when there's no garbage referenced by 5 of this thread's commits
- motivating issue #11911 runtime: unbounded heap growth with concurrent GC referenced by 2 of this thread's commits
Design. 3c3be62201 (Go 1.3, Dmitry Vyukov) moved sweep out of the stop-the-world pause: a background bgsweep goroutine plus lazy on-demand sweeping. The commit explains why this was hard: there is no point in time at which the live heap size or consistent malloc/free counts are known, so next_gc, mprof and memstats all become approximate. At the end of GC next_gc is set conservatively to heap_alloc*GOGC, and after each sweep it is decremented by freed*GOGC; the heap must not grow beyond the true value, enforced by two rules: sweeping a span whenever a span is allocated from an MCentral, and sweeping until N pages are freed whenever N pages are requested from MHeap. mprof needed a 3-generation scheme because frees for the previous GC are only known at the end of the next. Each span carries a sweepgen (set atomically before updating span state, 3590d9ce69), and MSpan_EnsureSwept guarantees a span is swept before use (6e612ae0f5). It was disabled the next day (3cac829ff4) and re-enabled after a fix for sweep with preemption on (f8e4a2ef94), followed by a string of race fixes in bgsweep (fed5428c4a, f8c350873c, ea87501750, 6e612ae0f5).
Concurrent mark in 1.5 broke the 1:1 rule (sweep one span per allocated span) because GC now starts earlier: 24a7252e25 finishes sweeping before a cycle starts, fc9ca85f4c makes sweep proportional to span bytes allocated (with a sweep distance, e30c6d64ba), later 7407d8e582, 6383fb6152 and 87d939dee8 fix over-aggressive credit and heap_live underestimates. In 1.8 sweep and root marking of spans became proportional to in-use spans rather than all spans ever allocated (f9497a6747, c95a8e458f, 575b1dda4e), fixing #9265 (a heap blown up to 8 GB then shrunk ran 60x slower). In 1.9 proportional sweep was driven directly by heap_live (a5eb3dceaf) instead of its own byte count and reimbursement. In 1.12 the sweeper gained a span mark bit (ba1698e963) and an efficient page reclaimer (5333550bdc) that uses a per-page in-use bitmap (69e666e4f7) and the arena list (e500ffd88c) to find fully-free spans without sweeping arbitrary spans.
Timeline.
- Go 1.3 — Done: concurrent sweep with
bgsweepand lazy sweeping (3c3be62201),MSpan.needzeroinstead of writing into span data (86e3cb8da5), races fixed (73a304356b,f8e4a2ef94,fed5428c4a,f8c350873c,6e612ae0f5,ea87501750,8d321625fdspans ending in wrong lists after split); stack shrink after sweepgen increments (e9445547b6). Left open:next_gcand heap accounting only approximate during the sweep (aea99eda0ffixed a runaway-memory case). - Go 1.4 — Done:
sweepgenset before span state (3590d9ce69); transfer cache so a swept span does not bounce between mcentral/mheap locks (e0df11d57e: the previous dance was lock mcentral, remove, unlock, unmark, lock mheap, insert, unlock, lock mcentral). Left open: nothing noted. - Go 1.5 — Done: bgsweep started eagerly (
5789b28525); sweep finished before concurrent GC starts (24a7252e25); proportional sweep on span bytes (fc9ca85f4c,e30c6d64ba). Left open: credit accounting was fragile (underflows fixed in 1.6). - Go 1.6 — Done: remove the in-use page loop and the sweep-wait loop from the STW phases (
dac220b0a9,9a31d38f65); sweep termination decentralised (9630c47e8c,a51905fa04); credit fixes (7407d8e582,6383fb6152,87d939dee8,c1cbe5b577,ff5c945382); simplifiedmSpan_Sweep(70462f90ec). Left open: nothing noted. - Go 1.8 — Done: sweep and
markrootSpansproportional to in-use spans (f9497a6747,c95a8e458f,575b1dda4e);mSpanList/go:notinheapclean-ups (991a85c889,1bc6be6423). Left open: nothing noted. - Go 1.9 — Done:
heap_live-driven proportional sweep (a5eb3dceaf,9d36163c0b),sweeponetracking (44ed88a5a7), sweep allocation counting simplified (b50b728587), trace events cover the whole sweep loop (79c56addb6). Left open: nothing noted. - Go 1.12 — Done: sweepone simplified and
gosweeponeremoved (06afc8b152,f3bb4cbfd5), span mark bit (ba1698e963), arena slice and in-use page bitmap (e500ffd88c,69e666e4f7), efficient page reclaimer (5333550bdc), large-allocation sampling fix (dc889025c7). Left open: nothing noted. - Go 1.15 — Done:
markrootSpansdriven by a per-page “has specials” bitmap rather than the sweep buffers (eacdf76b93); small-object sweeping while allocating bounded to 100 spans (08bf64a81e); zombie-slot detection (ea2de3346f); every marked object also marks its span (886caba73c, fixing missed marks fromgcmarknewobject/wbBufFlush1). Left open: nothing noted. - Go 1.17 — Done: sweep completion unified:
runtime.GCnow blocks on all sweep paths (a25a77aed2,1b736b3c19). Left open: nothing noted.
Concurrent mark: write barriers and the dev.garbage collector
Span: Go 1.4 → Go 1.6 · Status at go1.17: still the structure of the collector (concurrent mark with write barriers, 25% utilisation goal, assists); the barrier flavour changed in 1.8 (hybrid barrier) → continues as Sweeper termination and pacing and Mark worker scheduling and GC work queues (1.18+)
Paths not taken: Request-oriented collector (ROC) (never landed)
- design doc Go 1.4+ GC Plan and Roadmap listed in Go: A Documentary
- discussion Sub-millisecond GC pauses golang-dev
- talk Go GC: Solving the Latency Problem. GopherCon 2015. Jul 8, 2015. listed in Go: A Documentary
- background Rick Hudson. Getting to Go: The Journey of Go's Garbage Collector. 12 July 2018. listed in Go: A Documentary
- background Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens. 1978. On-the-fly garbage collection: An exercise in cooperation. Commun. ACM 21, 11 (November 1978), 966–975. listed in Go: A Documentary
Design. Go 1.4 shipped write-barrier call sites (compiler-inserted, no-ops) while the collector was still stop-the-world. The [dev.garbage] branch (merged for 1.5) added the machinery in steps: concurrent stack scan of goroutines (6184f46ea3, “does not scan running goroutines yet”, finished by a STW with stack rescan), all multi-word barriers expressed via writebarrierptr (b035e97315, which checks the value is not a small non-zero integer to flush out misuse and added the missing barrier for builtin copy), the real barrier and a switch (“if false in func gogc”, d3d60975b4), a coarsened barrier that always greys the destination (28c5385965), checkmark fixes (0584312e7d), and finally concurrent GC on by default with GC-internal structures such as free lists exempt from barriers (8cfb084534). The write barrier was then fixed to a single load-and-compare (32d6fbcb4f, needwb() becomes writeBarrierEnabled) so it can be inlined; barrier code moved to mbarrier.go (4d226dfee9); multi-word barriers use the heap bitmap (4212a3c3d9 typedmemmove, 65c4d7beab heapBitsBulkBarrier).
The phases as they existed in 1.5: sweep termination, concurrent scan (roots), mark 1, mark 2, mark termination. Barriers were enabled by a stop-the-world, replaced by a ragged barrier (bb6320535d) that only requires each P to observe the flag; then moved earlier to the scan phase because stack barriers assumed barriers were on (1303957dbf). Mark 2 (“mark termination preparation”) existed to drain local work caches before STW and was later removed (see 1.12). Mark termination did most of the sweeping setup and finalizer-root and span-root marking; 572f08a064 split span roots into 128 subtasks and 608c1b0d56 moved finalizer-object scanning to concurrent scan, cutting pauses by about 25% vs tip and 50% vs 1.5.1.
Timeline.
- Go 1.4 — Done: compiler-inserted write-barrier call sites (a no-op barrier;
b035e973151.5’s message refers to barriers inserted “for Go 1.4”). Left open: collection still STW. - Go 1.5 — Done: dev.garbage landing: concurrent scan (
6184f46ea3), barrier plumbing (b035e97315,d3d60975b4,28c5385965,9eded54fa3), default on (8cfb084534,273507aa8fstops running Gs during scan);GODEBUG=wbshadowto find missing barriers (dcec123a49, later removed1635ab7dfe) and checkmark mode on by default (9626561030); the GC internals reorganised intogcWork(b30d19de59,02dcdba7c8),gcMark/gcSweepsplit (89a091de24),mbarrier.go,mbitmap.go(3965d7508e),gcWorkper-P caches (1b4025f4bd); mark runs on a regular stack (fa2f9c2c09) with preemption allowed during concurrent phases (af060c3086); ragged barrier for barrier enabling (bb6320535d); prompt start (4b956ae317) and aggressive ending of mark (90a19961f2);GODEBUGgcshrinkstackoff,gcstackbarrieroff,gcstoptheworld(d5b40b6ac2);invalidptr=1default (d3ffc975f3) with bad-pointer check re-enabled (4addec3aaa, afterd57056ba26stopped freeing stack spans during GC). Left open: pause still includes rescanning all stacks and globals in mark termination; coordination via a single GC goroutine (decentralised in 1.6). - Go 1.6 — Done: concurrent scan and mark-2 root rescans performed by GC workers (
82d14d77da,fbf273250f); span roots, data/BSS root marking partitioned into 256K chunks (d3df04cd8c); finalizer-bearing objects scanned concurrently (608c1b0d56); STW loops over all spans removed (dac220b0a9,9a31d38f65); work buffers 256 B → larger (1870572180), two cached workbufs per gcWork to cut flapping (b6c0934a9b),work.partialremoved (feb92a8e8c), eager balancing of buffers (9439fa1078);GODEBUG=cgocheck=2uses the barrier to detect Go pointers written to non-Go memory (be1ef46775) user-visible. Left open: stack re-scan in mark termination still O(# stacks); fixed in 1.7 (2a889b9d931.7) and removed in 1.8 (hybrid barrier).
Decentralized GC state machine and mark termination simplification
Span: Go 1.6 → Go 1.12 · Status at go1.17: still the design at go1.17 (gcStart/gcMarkDone/gcMarkTermination run by whichever goroutine notices the transition; checkmarks mode independent of STW code) → continues as Mark worker scheduling and GC work queues (1.18+)
- design doc Proposal: Decentralized GC coordination Austin Clements, 2015-10-25 · listed in Go: A Documentary
- tracking issue #11970 runtime: replace GC coordinator with state machine referenced by 6 of this thread's commits
- design doc Proposal: Simplify mark termination and eliminate mark 2 Austin Clements, 2018-08-09 · linked from #26903
- proposal #26903 runtime: simplify mark termination and eliminate mark 2 referenced by 14 of this thread's commits
- motivating issue #12041 runtime: redesign GC synchronization referenced by 2 of this thread's commits
Design. Through 1.5 a dedicated GC coordinator goroutine ran every cycle (gc()), blocking on notes. 1.6 turned it into a distributed state machine: the off→mark transition (f54bcedce1, a transition lock and a condition similar to shouldtriggergc()), decentralised sweep termination (9630c47e8c, a51905fa04), mark completion in the scheduler removed (12e23f05ff), gcMarkDone and mark termination (c99d7f7f85), putfull starting mark workers (dcd9e5bc0f), getfull barrier removed from concurrent mark (62ba520b23) and finally the background GC goroutine and mark barriers deleted (d5ba582166). The state variables moved into var work (1959621584). Assist blocking changed from a 100 µs sleep loop to a park/ready queue (15aa6bbd5a).
Go 1.12 completed the cleanup by eliminating the sources of mark work during mark termination: flush the write barrier buffer to create work and track whether any gcWork buffer was flushed (9c634ea889, a2a2901b25), then remove mark 2 itself because disabling caches was slow and there was still a rare race (about once per all.bash) entering mark termination with work remaining (9108ae7751), remove gcBlackenPromptly (d398dbdfc3), STW mode re-implemented in terms of concurrent GC (1678b2c580, 918ed88e47), checkmarks decoupled from STW infrastructure (ecc365960b), GODEBUG=gctrace=2 and gcrescanstacks=1 debug modes removed (edc2d17086, 198440cc3d), blocking drains and the second root pass removed (457c8f4fe9, 550dfc8ae1), gchelper eliminated (0906d648aa), mcaches flushed lazily instead of in mark termination (873bd47dfb). A diagnosed-but-unexplained “P has cached GC work” failure (#27993) was worked around by re-checking for work after STW and restarting the world into concurrent mark (95a6f112c6), with extensive debug support (438b9544a0, fe2feb978e, 3c255e8bc6, 2d00007bdb, 9098d1d854, ccbca561ef, b8fad4b33d, 8e6396115e).
Timeline.
- Go 1.6 — Done: decentralised start, sweep termination, mark done (
f54bcedce1,9630c47e8c,a51905fa04,12e23f05ff,c99d7f7f85,171204b561,3842596284), GC coordinator goroutine removed (d5ba582166,bbf2da00fc),gcModetype (fbd2660af3),GCstwphase removed (a42f668654), state lifted intowork(1959621584,b0d5e5c500,3cd56b4dca). Left open: mark 2 and gchelper still present. - Go 1.7 — Done: only per-P gcWork (
0168c2676f),markrootDonegeneralised (c14d25c648); reset mark state before STW (9ab9053344). Left open: nothing noted. - Go 1.9 — Done: user-visible
runtime.GC()anddebug.FreeOSMemorynow trigger a concurrent cycle (9ffbdabdb0,786eb5b754,3d58498fdb,29fdbcfea3) instead of STW, with forced GCs tracked independently ofgcMode; heap profile snapshotted in mark termination (eee85fc5a1,1be3e76e76); finalizer queue not rescanned (f1ba75f8c5). Left open: STW GC mode remained as a separate code path. - Go 1.10 — Done: user-visible
runtime.GC()forces a cycle even with GOGC=off (0744c21b98);SetGCPercent(-1)waits for concurrent GC (64b68bedc5); STW duration measured more accurately (250a9610a4), gctrace CPU fix (89b7a08aea). Left open: nothing noted. - Go 1.12 — Done: mark-termination cleanup listed above (
9c634ea889,a2a2901b25,9108ae7751,d398dbdfc3,918ed88e47,1678b2c580,e25ef35254,ecc365960b,edc2d17086,198440cc3d,457c8f4fe9,550dfc8ae1,873bd47dfb,0906d648aa,578667f4b5preemptible mark termination goroutine);gListtypes for G queues (3578918b66,d02169d380,083938df14). Left open: “P has cached GC work” cause not found, only worked around (95a6f112c6). - Go 1.14 — Done: STW GC releases worldsema before Gosched (
05511a5c0a);g.gcscanvalidremoved (8c5861576a). Left open: nothing noted. - Go 1.15 — Done: dedicated mark workers can be preempted for STW (
79b43fa819), clearing the way to allow STW during mark;gcStartpreemption window closed (03ba6b070d). Left open: nothing noted. - Go 1.16 — Done:
gcBgMarkWorkerpreemption simplified (256d729c0b),debugCachedWorkremoved (2517f4946b). Left open: nothing noted.
GC pacer: trigger controller, assists and background utilisation
Span: Go 1.5 → Go 1.17 · Status at go1.17: still the design at go1.17 (trigger-ratio feedback controller, scan-work based assists, 25% background utilisation, soft/hard goal) but now isolated in mgcpacer.go as gcControllerState; → continues as GC pacer redesign (1.18+)
- design doc Go 1.5 concurrent garbage collector pacing listed in Go: A Documentary
- discussion Proposal: Garbage collector pacing golang-dev · listed in Go: A Documentary
- design doc Proposal: Separate soft and hard heap size goal Austin Clements, 2017-10-31 · listed in Go: A Documentary
- motivating issue #14951 runtime: mutator assists are over-aggressive, especially at high GOGC referenced by 3 of this thread's commits
- proposal #44167 proposal: runtime: GC pacer redesign referenced by 8 of this thread's commits
- design doc GC Pacer Redesign Michael Knyszek · linked from #44167
- tracking issue #42430 runtime: GC pacer problems meta-issue listed in Go: A Documentary
Design. Before 1.5 the GC trigger was just next_gc, the heap size at which a STW collection began. Go 1.5’s concurrent GC needed a pacer, and Austin Clements built it in March–August 2015 in mgc.go. Mechanics: the collector tracks heap_live (bytes allocated since the last mark termination plus bytes marked, d7e0ad4b82), marked heap size (50a66562a0), scan work (571ebae6ef, 4e9fc0df48, 8e24283a28), and sets a trigger below a goal (6d12b1780e makes next_gc the heap size at which GC triggers, while the goal is tracked separately). Mutators that allocate during GC must pay in scan work: proportional mutator assist (4b2fde945a) computes scan-work-per-allocated-byte at cycle start from the estimated scan work and the distance to the goal; background workers are scheduled to hit a 25% utilisation target (8d03acce54: dedicated workers plus a fractional worker, idle workers for idle Ps). A proportional-response controller (a0452a6821) replaces the fixed trigger of 7/8*GOGC growth, adjusting the trigger cycle to cycle from the observed heap growth and GC CPU use. Further 1.5 refinements: assist harder when the marked heap exceeds the estimate (170fb10089), reachable heap estimate (4655aadd00, later removed as unstable c8bd293e56), scan work estimated from heap scan size (17db6e0420, with scannable-bytes tracking 3be3cbd548, scalar slots counted 53c53984e7), heap minimum based on GOGC (b6e178ed7e, with a 4 MB floor 813e97b786), assists retried until paid off (8f34b25318), assists allowed during GC start and shutdown (fb5230af8a), revised assist ratio that accounts for heap allocated during the cycle (1fb01a88f9, e3870aa6f3), and GCCPUFraction in MemStats (23e4744c07) user-visible.
1.6 changed assist bookkeeping: per-G assist balance tracked directly (89c341c5e9; the old scheme needed floating point so it could only be checked at span allocation and Gs could run “in the red”), assist before allocating (65aa2da617), preemptible assists (456528304d), park/ready queue (15aa6bbd5a), minimum heap distance via heap goal (9e77c89868). 1.7 allocated black during GC (6002e01e34) and dropped the floating-garbage estimate (c8bd293e56). 1.8 disentangled next_gc from the trigger: MemStats.NextGC is the goal, the trigger is separate (ec9c84c884, 2098e5d39a), and assists can do root jobs (d35dfd405e). 1.9 generalised the trigger (29be3f1999), rationalised triggerRatio (49a412a5b7), consolidated trigger-derived computations (ee175afac2, 1c4f3c5ea0), and stopped adjusting the trigger on forced GC (2919132e1b) or SetGCPercent (227fff2ea4). 1.10 separated soft and hard heap limits (03eb9483e3): until then assists assumed the entire heap was live, so with GOGC=100 the collector worked twice as hard as needed and finished halfway between trigger and goal; the soft goal uses the expected live fraction while the hard goal bounds the worst case. Together with allowing 5% assist beyond the 25% background goal so the controller still gets feedback (af192a3e22) and separating background from goal utilisation (27923482fa), only dedicated workers at reasonable GOMAXPROCS (315c28b788), fractional worker preemption (28e1a8e47a, b783930e63, e09dbaa1de). 1.13 introduced an effective GOGC concept to remove heap_marked hacks (5c22842cf2, 4a7d5aa30b, 7da03b9fbb). 1.14 placed a lower limit of 0.6 on the trigger ratio because the faster page allocator had driven it down to 0.07, which causes huge black allocation (74af7fc603) and fell back to the hard goal if more scan work than expected had been done (aae0b5b0b2). 1.15 ensured min trigger ratio never exceeded max (d1ecfcc1e8). 1.16 made scan-work and assist-ratio accesses atomic (93d7d1685e, ce46f197b6, 8cc280aa72). 1.17 split the pacer into mgcpacer.go and made its state methods of gcControllerState (f5f7647107, 728e3dc6f9, 9bce7b70fd, e7ab1a5ba8, 7ec7a3cf33, 2d4ba2601b, f2d5bd1ad3, e9cc31e736), the preparation for the 1.18 redesign.
Timeline.
- Go 1.5 — Done: scan-work accounting, proportional assist, utilisation-scheduled workers, trigger controller,
heap_live(4b2fde945a,8d03acce54,a0452a6821,d7e0ad4b82,571ebae6ef,4e9fc0df48);gctracereports heap sizes and trigger (8c3fc088fb,cb10ff1ef9,1ccc577b8a,dc870d5f4b,10083d8007,cc6ed285e5,babeb4a963);gcpacertrace(f9dc3382ad). Left open: utilisation controller still based on an estimate of floating garbage, assists computed once at cycle start and revised only by heuristics; mark 2 and small workbufs cause pacing noise (fixed 1.6–1.12). - Go 1.6 — Done: direct per-G assist balance, assists before allocating, preemptible, park/ready (
89c341c5e9,65aa2da617,456528304d,15aa6bbd5a), heap distance (9e77c89868), proportional sweep fixes (7407d8e582,6383fb6152,87d939dee8). Left open: nothing noted. - Go 1.7 — Done: allocate black plus counting black allocations toward scan work (
6002e01e34,479501c14c,64a26b79ac); floating garbage estimate removed (c8bd293e56); reduced assist extra credit (ccca9c9cc0); sweep ratio in gcpacertrace (17f6e5396b). Left open:next_gcstill the trigger not the goal. - Go 1.8 — Done: user-visible
MemStats.NextGCis now the goal heap (ec9c84c884),heap_reachableremoved (2098e5d39a); assist fixes (984753b665,81c431a537,f9e1adb713,d35dfd405e);NumForcedGCuser-visible (01c6a19e04). Left open: assists still assume entire heap live. - Go 1.9 — Done: trigger generalised and recomputed consistently (
29be3f1999,49a412a5b7,ee175afac2,1c4f3c5ea0,a5eb3dceaf), no GC on SetGCPercent (227fff2ea4). Left open: soft/hard limit separation next release. - Go 1.10 — Done: soft and hard heap limits, 5% assist headroom, utilisation separation (
03eb9483e3,af192a3e22,27923482fa,315c28b788,28e1a8e47a,b783930e63,e09dbaa1de). Left open: nothing noted. - Go 1.13 — Done: effective GOGC and no GOGC in minimum sweep distance (
5c22842cf2,4a7d5aa30b,7da03b9fbb). Left open: nothing noted. - Go 1.14 — Done: trigger-ratio floor 0.6 (
74af7fc603), hard goal fallback (aae0b5b0b2). Left open: the trigger ratio design needed rethinking (1.18). - Go 1.16 — Done: atomic scan work/assist ratio and defined synchronisation on
heap_scan(93d7d1685e,ce46f197b6,8cc280aa72). Left open: nothing noted. - Go 1.17 — Done: pacer extracted into
mgcpacer.gowithgcControllerStatemethods (f5f7647107,f2d5bd1ad3,728e3dc6f9,2d4ba2601b,9bce7b70fd,e7ab1a5ba8,7ec7a3cf33,e9cc31e736), minimum scavenge fraction constant fixed (14ade57ab8). Left open: the trigger-ratio model itself; this refactor is the preparation for the 1.18 pacer redesign.
Stack scanning: conservative, precise by frames, stack barriers, hybrid barrier, async safe points
Span: Go 1.1 → Go 1.14 · Status at go1.17: stack roots scanned precisely using stack maps and stack objects, with no rescan, plus conservative scanning at async safe points; → continues as Stack and register root scanning (1.18+)
- design doc Precise stack roots listed in Go: A Documentary
- discussion Precise garbage collection of stack roots golang-dev · listed in Go: A Documentary
- design doc GC scanning of stacks listed in Go: A Documentary
- tracking issue #17503 runtime: eliminate stack rescanning referenced by 4 of this thread's commits
- design doc Proposal: Eliminate STW stack re-scanning Austin Clements, Rick Hudson, 2016-10-21 · linked from #17503
- design doc Proposal: Concurrent stack re-scanning Austin Clements, Rick Hudson, 2016-10-18 · listed in Go: A Documentary
Design. In 1.0 stacks were scanned conservatively word by word. The precision sequence: 1.2 first enabled scanning by frames using stack maps (0e6007e4f9) and in the same release disabled it again because walking frames was about 3x more expensive and the maps were not complete enough to be precise (c3dadca977: “the conservative choice… same as Go 1.1”; benchmarks were added to re-enable it in 1.3); 1.3 moved stack scanning into the parallel mark phase to amortise the cost (0368a7ceb6, noting remaining cost in gentraceback); 1.4 dropped the remaining conservative mode (d208361cde “remove uses of ScanConservatively”) and caches (b91223edd1 cached unrolled GC bitmask). Concurrent stack scan arrived in 1.5 (6184f46ea3, 273507aa8f scan states), with a re-scan in mark termination for stacks mutated after being scanned. To shorten that rescan Go 1.5 added stack barriers (3f6e69aca5 steals stack space for barrier bookkeeping, 306f8f11ad unwinds barriers when writing above the current frame, e9089e4ab6 and 92e68c89f6 document and isolate the code): the runtime hijacks return addresses in frames, so only frames above the lowest barrier hit need rescanning. 1.7 made rescan O(# dirty stacks) rather than O(# stacks) (2a889b9d93: about 34 ns per clean-stack job, exceeding the 10 ms goal near 300k goroutines), stopped rescanning globals (b49b71ae19: up to 500 µs per MB in STW), freed dead G stacks concurrently (e8337491aa), shrunk stacks during concurrent mark instead of mark termination (f11e4eb5cc, 5 ms/MB copied) and removed stack barriers during mark and sweep (269c969c81, 9f263c14ed).
The hybrid barrier in 1.8 (see next thread) made all stack rescanning unnecessary: bd640c882a disabled it by default with a gcrescanstacks GODEBUG as fallback, c5ebcd2c8a removed the rescan list in 1.9, 198440cc3d removed the debug mode in 1.12, 7bc42a145a stopped reserving stack space for barriers, and 8c5861576a removed g.gcscanvalid in 1.14. Stack scanning was made lock-friendly: d6625caf53 scans mark worker stacks like any other. 1.11 and 1.13 added missing roots found by the new clobberfree mode and stack objects: gp._panic (af8f4062c2) and defer closures (4f4c2a79d4); 1.14 added d16ec13756 (conservative scan of registers dumped by asyncPreempt and its parent frame, and propagation of conservativeness to stack objects whose pointers were found conservatively) with 6058603471 deferring stack shrinking to synchronous safe points.
Timeline.
- Go 1.2 — Done: stack-by-frames scanning enabled (
0e6007e4f9) and then disabled (c3dadca977). Left open: cost ofgentracebackand incomplete precision. - Go 1.3 — Done: stack scan in parallel mark (
0368a7ceb6). Left open:gentracebackinefficiency. - Go 1.4 — Done: conservative scanning of stacks removed for Go frames (
d208361cde), shrunk stacks freed only after GC (91e8554b8b,d57056ba261.5 later), parallel shrink (192bccbf33). Left open: stack rescan in STW. - Go 1.5 — Done: concurrent stack scan and stack barriers (
6184f46ea3,3f6e69aca5,306f8f11ad,7529314ed3,724f8298a8,3c60e6e8cfrace fixes,22e57c66551.6 locking,9c9d74aba71.6 no sigprof during barrier ops,a7c09ad4fb1.6);GODEBUGgcstackbarrieroff(d5b40b6ac2) and every-frame barrier (3bfc9df21a1.6, 1.6). Left open: rescan during mark termination remained O(stacks). - Go 1.7 — Done: O(dirty stacks) rescan, no global rescan, concurrent shrink (
2a889b9d93,b49b71ae19,f11e4eb5cc,e8337491aa,269c969c81,9f263c14ed,efb0c55407span root marking out of mark termination). Left open: rescan still needed. - Go 1.8 — Done: rescan disabled by default (
bd640c882a), mark worker stacks scanned normally (d6625caf53), consistency checks for rescan position (ab59235729). Left open:gcrescanstacksfallback kept for one release. - Go 1.9 — Done: rescan list removed (
c5ebcd2c8a). Left open:g.gcscanvalidkept. - Go 1.12 — Done:
gcrescanstacksremoved (198440cc3d). Left open: nothing noted. - Go 1.13 — Done:
gp._panic, defer closure scanned (af8f4062c2,4f4c2a79d4) withclobberfree; stack freeing separated from shrinking (68d89bb8e0). Left open: nothing noted. - Go 1.14 — Done:
gcscanvalidremoved (8c5861576a); shrink only at synchronous safe points (6058603471); conservative scan at async safe points (d16ec13756); markroot overflow fixed (66e562cc52). Left open: async preemption’s own injection is in the scheduler area. - Go 1.17 — Done: stack-object metadata emits only GC data instead of the full type descriptor, avoiding keeping the descriptor and everything it references alive (
02ab8d1a1d); GC testing helpers for moving stacks (4e1bf8ed38,97b3ce430b,41e8a9f1cf). Left open: nothing noted.
Hybrid write barrier and buffered write barrier
Span: Go 1.8 → Go 1.12 · Status at go1.17: still the design at go1.17 (deletion+insertion “hybrid” barrier with a per-P pointer buffer flushed through wbBufFlush) → continues as Stack and register root scanning (1.18+)
Paths not taken: Non-moving generational GC with card marking (never landed)
- design doc Proposal: Eliminate STW stack re-scanning Austin Clements, Rick Hudson, 2016-10-21 · linked from #17503
- tracking issue #17503 runtime: eliminate stack rescanning referenced by 6 of this thread's commits
- tracking issue #22460 runtime: optimize write barrier referenced by 8 of this thread's commits
- background T. Yuasa. 1990. Real-time garbage collection on general-purpose machines. J. Syst. Softw. 11, 3 (March 1990), 181-198. listed in Go: A Documentary
Design. The Yuasa deletion + Dijkstra insertion hybrid barrier (5380b22991) shades both the old value in the slot and (unconditionally for now, because channel ops would need both stacks’ state) the new pointer. Prerequisites landed in 1.7–1.8: barrier before the pointer write (8f81dfe8b4, so the barrier can observe the old slot value), fixalloc zeroing on reuse (ae3bb4a537, to avoid seeing a dead heap pointer), typed memclr operations invoking the barrier, bulkBarrierPreWrite simplification (d8256824ac), and tiny blocks marked at GC start (85c22bc3a5). Effect: worst-case mark termination at GOMAXPROCS=12, excluding stop-the-world time itself, reliably under 100 µs with a 95th percentile around 50 µs (message of bd640c882a). Allocation during GC is black (6002e01e34) which the hybrid barrier requires for stacks.
Go 1.10’s buffered write barrier (e9079a69f3, amd64) replaces the per-write call with a fast path that enqueues the pointers in a per-P buffer without spilling general registers or placing arguments on the stack; only when the buffer fills does the slow path spill all registers and flush (no safepoints or stack splits while the frame is active). Prerequisites: eliminating write barriers from runtime internals that run inside it (d941b07558 persistentalloc, 164e1b8477 recordspan, 3526d8031a gchelper, 070cc8eb02 startpanic_m, 249b5cc945 gcWork methods nowritebarrierrec, 98da2d1f91 wbufptr removal, 42c1214762 alloc/mark bitmaps). bulkBarrierPreWrite (typedmemmove/typedmemclr) then used the buffer (877387e38a: 7.33 → 4.46 ns, buffer of 256), typedslicecopy became one bulk barrier + memmove (6a5f1e58ed), and growslice used the same trick in 1.12 (96dcc4457b, 4363c98f62); in 1.11 all writebarrierptr* calls were eliminated (245310883d). In 1.12 the buffer is poisoned during flushing (fe2feb978e).
Timeline.
- Go 1.8 — Done: hybrid barrier (
5380b22991), pre-write ordering (8f81dfe8b4), 32-bit barrier flag load (fdc4a964d2,8107b0012f1.7), idle-worker and assist improvements (see pacer). Left open: the barrier is unconditional “for now” because channel operations would need both stacks’ state (5380b22991). - Go 1.9 — Done: write barriers eliminated from allocation/mark bitmaps (
42c1214762),wbufptrremoved (98da2d1f91), comment on compiler knowledge ofwriteBarrier(7e1832d06c). Left open: barrier still a normal call. - Go 1.10 — Done: buffered barrier (
e9079a69f3), bulk barriers buffered (877387e38a),typedslicecopyrewrite (6a5f1e58ed), reset on all flush paths (043f112e52),heapBits.bitsnosplit (3675bff55d). Left open: fast path implemented only for amd64 per the commit message. - Go 1.11 — Done:
writebarrierptrcalls eliminated (245310883d), simplifiedbulkBarrierPreWrite(3e214e5693), short-circuit typedmemmove/typedslicecopy for dst==src (486caa26d7,3658299f44). Left open: nothing noted. - Go 1.12 — Done: barrier buffer flush creates GC work (
9c634ea889), poisoning (fe2feb978e), growslice bulk barrier (96dcc4457b,4363c98f62). Left open: nothing noted.
Precise heap scanning: type information, the heap bitmap and GC programs
Span: Go 1.1 → Go 1.17 · Status at go1.17: still the design at go1.17 (2-bit/1-bit-per-word sparse per-arena heap bitmap written by heapBitsSetType, GC programs for large types, mark bits in separate per-span bitmaps) → continues as Heap bitmap rewrite and allocation headers (1.18+)
Design. Until 1.1 the collector treated heap blocks conservatively except for flags set at allocation (noscan vs scan). Jan Ziak’s 9204eb4d3c (2013-01, Go 1.1) made the collector interpret type information: when a block’s type is known, the marker walks its pointer layout rather than testing every word; untyped blocks have the collector try to determine the actual type (059fed3dfb); interface values are scanned through their dynamic type (92c153d5f4, 21ea5103a4), channel types are supported by cmd/gc (6e69df6102), and cfe336770b/60682c4f59 replace unions by structs since unions break precise GC. A checker for heap type info (bd1cd1ddea) and a hook to disable precise GC for bisecting problems (709e03f43d) accompanied it; c6293d2106 fixed scanning a slice pointing at an array embedded in a struct, and 54c901cd08 fixed s[len(s):] string pointers that point one past the allocated block. In 1.3 settype writes became lock-free: each MCache owns a span per size class so bitmap updates need no CAS (3b5278fca6).
Go 1.4’s bitmap was a byte array of 4 bits per heap word (ff3fa1b32d, “half the code accesses the bitmap as bytes”), with a cache of unrolled masks for types (b91223edd1, up to 4K cached; perf showed 2% in unrollgcprog). fb44fb6cb7 made type mandatory for scannable allocations. When the allocator and collector moved to Go in 1.5 (1e2d2f0947, automated conversion, followed by 3965d7508e factoring mbitmap.go), 122384e489 removed the boundary bit (finding object boundaries from span element size instead of touching random heap bits; this slowed the garbage benchmark 2%), allowing the bitmap to shrink to 2 bits per word. e375ca2a25 reordered bits as mmmmpppp so 1-bit pointer masks convert directly; feb8a3b616 is the real heapBitsSetType that avoids two atomics per 2-bit field; 4212a3c3d9 makes typedmemmove use the bitmap; ecfe42cab0 keeps pointer bits always set in one-word spans; 9feb24f3ed and 899a4ad47e speed heapBitsForObject (multiply instead of divide; power-of-two masks). 1.5 bitmap bugs: a8ae93fd26 (repeating with large scalar tails), d231cb8249 (slice of GC program repeats n-1 times), 5bfe8adee5 (1.4).
In 1.7 the mark bit left the heap bitmap for separate per-span mark/alloc bit vectors (see allocation thread), freeing the “scan/dead” bit in the first word (a20fd1f6ba reclaims it, simplifying 1/2/3+ word cases) and in 1.9 noscan objects got separate spans, so a noscan test is a span-class test (1a033b1a70, c44d031bf0 removes heapBits.hasPointers, 8e25d4ccef removes heapBitsSetTypeNoScan; 1% gain on go1 and x/benchmarks). 1.11 made the bitmap sparse: one bitmap chunk per 64 MB of heap, indexed through an arena index (c0392d2e7f, 29e9c4d4a4 lays it out forward in memory, f61057c497, b88276da66), splitting object finding from bit lookup (058bb7ea27: heapBitsForObject → findObject + heapBitsForAddr). 1.16 moved checkmarks from the encoded heap-bitmap bits into a separate bitmap (d19fedd180) and added a 24-byte class which required heapBitsSetType to handle objects that are not 16-byte aligned (14c7caae50).
Timeline.
- Go 1.1 — Done: precise heap scanning from types (
9204eb4d3c,059fed3dfb,92c153d5f4,6e69df6102,21ea5103a41.2 in 1.2), unions→structs (cfe336770b,60682c4f59),709e03f43ddisable hook, type-info checker (bd1cd1ddea). Left open: stack scanning and parts ofruntimedata stayed conservative. - Go 1.2 — Done: type info for interface values (
21ea5103a4), array/slice fix (c6293d2106),cnewarray(5782f4117d). Left open: nothing noted. - Go 1.3 — Done: lock-free settype (
3b5278fca6), empty-string pointer fix (54c901cd08), pointer-in-interface scan same as pointer (68aaf2ccda). Left open: nothing noted. - Go 1.4 — Done: byte-array bitmap (
ff3fa1b32d), unrolled mask cache (b91223edd1), backward scan removal (03f2189a1b), bitmap corruption fix (5bfe8adee5), type mandatory for scannable allocation (fb44fb6cb7). Left open: 4 bits per word and boundary bits. - Go 1.5 — Done: boundary bits removed and 2-bit bitmap (
122384e489,e375ca2a25,feb8a3b616,ecfe42cab0,266a842f55,4212a3c3d9); runtime converted to Go (1e2d2f0947,8420df622a);heapBitsForObjectfaster (9feb24f3ed,899a4ad47e,63caec5dee); GC-program fixes (a8ae93fd26,d231cb8249). Left open: mark bit and scan bit still in the heap bitmap. - Go 1.7 — Done: conditional branch removed from
heapBits.bits(4eb33f6b8d), clears reduced (0d26efb12a), first-word scan bit reclaimed (a20fd1f6ba), morePointers/isPointer without branches (a5d3f7ece9,d5e3d08b3a). Left open: noscan still checked through the bitmap. - Go 1.8 — Done: scanobject load/shift reduced (
0e7e43688d), atomics dropped fromheapBitsSetType(c03925edd3). Left open: nothing noted. - Go 1.9 — Done: noscan spans (
1a033b1a70,c44d031bf0,8e25d4ccef). Left open: nothing noted. - Go 1.11 — Done: sparse bitmap (
c0392d2e7f,29e9c4d4a4,f61057c497),findObjectsplit (41e6abdc61,058bb7ea27). Left open: nothing noted. - Go 1.15 — Done: only scan up to
ptrdata, not size (08dd4ad7e3),materializeGCProgrounding fix (ab5a40c5e3). Left open: nothing noted. - Go 1.16 — Done: checkmarks bitmap (
d19fedd180),heapBitsSetTypedoubleCheck simplified (7148abc1b9), 24-byte class support (14c7caae50). Left open: the bitmap itself (heap-wide, per-word) is what 1.20+ replaces. - Go 1.17 — Done:
scanobjecthbits advance folded into the loop (72483de87a). Left open: nothing noted.
Allocating from mark bits: allocBits, freeindex and ctz
Span: Go 1.7 → Go 1.10 · Status at go1.17: still the design at go1.17 (per-span allocBits/gcmarkBits, freeindex, 64-bit allocCache, nextFreeFast) → continues as Allocation publication, zeroing and preemptibility (1.18+)
- design doc Proposal: Dense mark bits and sweep-free allocation Austin Clements, 2015-09-30 · listed in Go: A Documentary
- tracking issue #12800 runtime: replace free list with direct bitmap allocation listed in Go: A Documentary
Design. Through 1.6 sweep built a free list per span by walking the mark bitmap (heapBitsSweepSpan), and malloc popped objects from mcache free lists. The [dev.garbage] work of Rick Hudson in 1.7 removed free lists. Each span has two bit vectors, one bit per object: allocBits and gcmarkBits. GC marks into gcmarkBits; after sweep, the vectors swap roles, so the mark vector becomes the allocation vector (3479b065d4, with helpers dc65a82eff, structs 2ac8bdc52a). freeindex marks the boundary below which all objects are allocated; a 64-bit allocCache holds the inverted allocBits starting at freeindex so that ctz64 finds the next free object (4093481523, 44fe90d0b3), later via the sys.Ctz64 intrinsic (2fb75ea6c6). e4ac2d4acc replaces ref by allocCount; 2063d5d903 restructures alloc and mark bits; 38f674687a reintroduces the no-zeroing optimisation using needzero (previously relying on the sweeper clobbering first two words), 8dda1c4c08 removes heapBitsSweepSpan and moves tracefree/msanfree to the point of reallocation, 6d11490539 restores allocfreetrace via an optional bitmap sweep, b3579c095e revives the sweep fast path, e9eaa181fc makes nextFreeFast inlinable (and 101fbc2c82 again in 1.10). gcBits arenas allocate the vectors (64770f642f); 1.9 made allocation of mark bits lock-free (2ef88f7fcf, 6c4a8d195b) and 1.12 added a span-level mark bit (ba1698e963).
Timeline.
- Go 1.7 — Done: the allocBits/freeindex/allocCache scheme (
2ac8bdc52a,e1c4e9a754,dc65a82eff,3479b065d4,4093481523,1354b32cd7,2063d5d903,2fb75ea6c6);stackfreelistadded (aed861038f);mspan.starteliminated forbase()(b7adc41fba,3e2462387f); allocation-count fixes (d97625ae9e); publication barrier even for noscan objects (d8b08c3aa4). Left open: sweep and bitmap code needed further simplification (1.9). - Go 1.9 — Done: lock-free fast path for mark-bit allocation (
2ef88f7fcf), simplified sweep allocation counting (b50b728587),markBits.clearMarkedNonAtomicremoved (6aee6b895c), mcentral cache-line alignment (259d60995d), division avoided in gc (a4ee95c805). Left open: nothing noted. - Go 1.10 — Done:
nextFreeFastinlinable (101fbc2c82), array-of-1 allocation special case (29e9b89b9a). Left open: nothing noted. - Go 1.12 — Done: span mark bit set whenever any object on the span is marked (
ba1698e963). Left open: gcmarknewobject path missed it, fixed in 1.15 (886caba73c1.15).
Size classes, tiny allocator and mcache/mcentral
Span: Go 1.1 → Go 1.17 · Status at go1.17: still the design at go1.17 (_NumSizeClasses = 68 entries at go1.17 including the 24-byte class, spanClass splitting scan/noscan, mcentral built on spanSet, mcache holds one span per span class) → continues as mallocgc decomposition and size-specialized malloc (1.24+)
- motivating issue #8885 runtime: consider adding 24-byte size class referenced by 1 of this thread's commits
- background Sanjay Ghemawat, Paul Menage. TCMalloc : Thread-Caching Malloc. Google Inc., 2009 listed in Go: A Documentary
Design. 1.0’s allocator was tcmalloc-like: size classes, MCache per M, MCentral per class, MHeap for pages. 1.1 coalesced zero-size allocations (ec59b840f9) and made mcentral allocation faster by anticipating how many objects are servable (d8626ef128). 1.2: with per-P mcaches, finer-grained transfers were harmful so a whole span moves from MCentral to MCache (23ad563119, noting a 4K span); size-to-class conversion inlined with an int8 table (5a89b35bca); MCache_Alloc inlined into mallocgc (5166013f75); MCache simplified (c4cfef075e, dropping tcmalloc nlistmin thresholds); malloc stats made cheap by counting only frees (5d637b83a9, c075d82cca). 1.3: 8K pages (6d603af6dc, prepared by 327e431057, landed as e48751e217; reduces metadata size and DTLB pressure, ~10% shorter pauses), the tiny allocator combining noscan allocations <16 B into one block (1fa7029425; 10+% fewer allocations on json/garbage; fixes ce884036d2, d18167fefe in 1.6, bf606094ee), slice growth rounded to the allocated block (bace9523ee), prefetch of the next block (d176e3d7c5, 99482f2f9e in 1.5). 1.4: free removed (cecca43804, because it conflicts with bump-the-pointer allocation, “we want to write the runtime in Go”), a transfer cache (e0df11d57e), and heap growth by 64K (0622e13b4d).
1.8 widened the size class set: classes now take the maximum size for the same page count and object count, e.g. class 36 grows from 1664 to 1792 (14e5951166: lists affected classes with gains of 5–12%) and the table is computed statically by mksizeclasses.go rather than at start (7ba36f4adb). 1.9 separated noscan spans and introduced spanClass (scan bit + size class; 295d160e01, 1a033b1a70), removing sizeToClass (7ee43faf78). 1.13 replaced division by element size with multiply and shifts (6ca51f7897). 1.15 gave mcentral a new implementation on spanSet (see below). 1.16 added the 24-byte class, which fits a 3-pointer slice header exactly (14c7caae50), and 12-byte objects on 32-bit systems are aligned to 8 (5756b35601, since the tiny allocator could otherwise break atomic 64-bit fields). 1.17 simplified divmagic for span object index computations to two 32-bit multiplies, safe thanks to bounds verified in mksizeclasses.go (4662029264), and fixed documented alignment of the 32K/64K classes (a8a85281ca).
Timeline.
- Go 1.1 — Done: zero-size coalescing (
ec59b840f9), faster mcentral (d8626ef128), earlier detection of unused spans (09ea3b518e), scavenged spans needing zeroing (9ad236ab72), distinct pattern marking free blocks needing zeroing (c80f5b9bf7). Left open: nothing noted. - Go 1.2 — Done: whole-span transfers, inlined classification and MCache_Alloc, simplified MCache (
23ad563119,5a89b35bca,5166013f75,c4cfef075e,c075d82cca),persistentallocfor small metadata (47e0a3d7b1,86da989ee5). Left open: nothing noted. - Go 1.3 — Done: 8K pages, tiny allocator, smarter slice growth, prefetch (
e48751e217,1fa7029425,bace9523ee,d176e3d7c5,d5a36cd6bbzero 2-word blocks in place). Left open: nothing noted. - Go 1.4 — Done:
freeremoved (cecca43804), transfer cache (e0df11d57e), alloc counted for tiny allocs fortesting.AllocsPerRun(e19d8a47d1). Left open: nothing noted. - Go 1.5 — Done: prefetch in allocation (
99482f2f9e), fix tiny allocator pointer past data (0e84088715), minimum heap 4 MB (813e97b786). Left open: nothing noted. - Go 1.7 — Done: user-visible
runtime.KeepAlive(6ab45c09f6),maxSliceCapfor division-free slice caps (a4dd6ea152, MakeSlice -32%). Left open: nothing noted. - Go 1.8 — Done: bigger size classes, static class tables (
14e5951166,7ba36f4adb), tiny blocks marked at GC start (85c22bc3a5). Left open: nothing noted. - Go 1.9 — Done:
spanClass, noscan segregation,sizeToClassremoval (1a033b1a70,295d160e01,7ee43faf78). Left open: nothing noted. - Go 1.12 — Done: mcache refill simplified (
2d23ece135,01e6cfc2a0), unneeded span tracking in mcentral dropped (70d7e96c78),newarrayuses multiplication with overflow check (1e50dd02a9). Left open: nothing noted. - Go 1.13 — Done: division → multiply/shift (
6ca51f7897). Left open: nothing noted. - Go 1.15 — Done: new mcentral (see span-set item);
OnesCount64for allocated counts (67c2dcbc59). Left open: nothing noted. - Go 1.16 — Done: 24-byte class, 12-byte alignment on 32-bit (
14c7caae50,5756b35601). Left open: nothing noted. - Go 1.17 — Done: simplified divmagic (
4662029264), alignment doc fix (a8a85281ca), tiny allocs top-aligned in race mode (2c05ba4ae0), large clears broken into chunks so preemption can occur (0bbfc5c31e). Left open: “huge” clears remain non-preemptible while a single clear runs (explicit in the commit message: zeroed by chunks “so that preemption can occur”).
Span lifecycle in mheap: free lists, treaps, first-fit and the page reclaimer
Span: Go 1.0 → Go 1.13 · Status at go1.17: replaced by the bitmap page allocator in 1.14 (see “The bitmap page allocator”); the in-use span bookkeeping (spanSet, page reclaimer, in-use bitmap) is still the design at go1.17
Design. 1.0’s MHeap kept free spans in per-page-count lists (free[], up to 128 pages = 1 MB) and one list for larger spans, plus MHeap_LookupMaybe through the heap map, coalescing neighbours on free. Problems: spans larger than 1 MB were on a linked list with O(n) insert/remove (heaps approaching 100 GB), fixed in 1.9 with a treap keyed by size (6e9ec14186, Rick Hudson). 1.12 removed the remaining mSpanList free lists (07e738ec32: now only treaps; “any number of spans totaling the right size” for small allocations, potentially increasing fragmentation), removed busy* lists (b00a6d8bfe), split scavenged from unscavenged spans (b46bf0240c, e508a5f072) and added an iterator (3651476075, 4b3f04c63b, predecessor/successor 5dd4d1f820, 239341f3b6). 1.13 changed the policy to address-ordered first-fit (40036a99a0: the treap becomes address-ordered and also tracks max free pages so find can first-fit; this also makes the scavenger go highest-address-first without extra code; 8c05d67661 fixed find), merged treaps into one (9baa4301cf), made splits in-place on the treap without removal (5c15ed64de), added treapIterFilter so iteration skips irrelevant subtrees (fa8470a8cd: scavenging could be N² otherwise), and provisioned huge-page-aware scavenging (see scavenger).
Other span bookkeeping: 1.8 consolidated h_spans and mheap_.spans (6b0f668044, 5915ce6674), made _MSpanDead zero (f4dcc9b29b) and introduced mSpanState (6879dbde4e), removed the allspans snapshot (575b1dda4e); 1.9 allocated workbufs out of manually-managed spans (9cc883a466) freed during sweep (051809e352) and excluded manual spans from heap_inuse/heap_sys (dc0f0ab70f); 1.12 added the arena slice (e500ffd88c). 1.13 treapForSpan (7b33b6274f) and 1.14 replaced the whole structure (next thread). 1.15’s spanSet + new mcentral (8af8fa9103, 9582b6e8fd, a13691966a, d1798d5aa0): the old mcentral was linked lists under a lock with a confusing ownership model (spans owned by some combination of a gcSweepBuf, a concurrent sweeper, an mcentral, an mcache), the new one keeps partial and full span sets per class, each a pair of spanSets for swept and unswept spans with lock-free push/pop on an atomic head-tail pair like sync.Pool; each span is owned by one source at a time. 39380e8e01 fixed a block leak caused by a race between two poppers.
Timeline.
- Go 1.0 — Done: span list-based heap with coalescing (
9f726c2c8ballspans), release unused memory (5c598d3c9f, see scavenger). Left open: nothing noted. - Go 1.3 — Done: specials hung off spans (
020b39c3f3), span-corruption fix on split (8d321625fd). Left open: nothing noted. - Go 1.4 — Done: mheap not locked on user goroutine (
e91704af27: the code under lock could grow the stack), allspans races (7f223e3b3b). Left open: nothing noted. - Go 1.5 — Done: handling of holes in the heap (
6d1ebeb527), reorganised memory code (484f801ff4), unusedsince fix (e604c6e293),mSpanListtype (1652a2c3161.6, 1.6). Left open: nothing noted. - Go 1.8 — Done: span map and state cleanups (
6b0f668044,5915ce6674,6879dbde4e,f4dcc9b29b,3cbfcaa4baisFree). Left open: nothing noted. - Go 1.9 — Done: treap for large spans (
6e9ec14186); manual spans for workbufs (9cc883a466,051809e352,dc0f0ab70f,9741f0275c). Left open: nothing noted. - Go 1.12 — Done: treap-only heap, separated scavenged spans, iterator, coalescing refactor (
07e738ec32,b00a6d8bfe,b46bf0240c,e508a5f072,3651476075,79ac638e41,2f99e889f0,6e9f664b9a,689fae2d78,011b8eb6bb). Left open: best-fit by size led to fragmentation; first-fit adopted in 1.13. - Go 1.13 — Done: first-fit, unified treap, in-place split, filtered iteration, treap iterator conversions (
40036a99a0,8c05d67661,9baa4301cf,5c15ed64de,fa8470a8cd,7bb8fc1033,7b33b6274f,a62b5723be,f4a5ae5594,ff70494b75). Left open: the heap lock held for all span operations, and a tree allocator that would not scale: replaced in 1.14. - Go 1.15 — Done:
spanSetand new mcentral (8af8fa9103,9582b6e8fd,a13691966a,d1798d5aa0,39380e8e01,eacdf76b93), leftover fields removed (9933f665551.16 in 1.16). Left open: nothing noted.
The bitmap page allocator
Span: Go 1.14 → Go 1.17 · Status at go1.17: still the design at go1.17 (radix-tree summaries over per-chunk 4 MiB allocation bitmaps, searchAddr, sparse chunk index, inUse address ranges) → continues as Scavenger redesign and Address-space layout, tagged pointers and the mem.go layer (1.18+)
Paths not taken: Scavenger and heap-lock experiments before the page allocator rewrite (superseded)
- design doc Proposal: Scaling the Go page allocator Michael Knyszek, Austin Clements, 2019-10-18 · linked from #35112
- tracking issue #35112 runtime: make the page allocator scale referenced by 14 of this thread's commits
- talk Evolving the Go Memory Manager's RAM and CPU Efficiency. Nov 11, 2020 listed in Go: A Documentary
Design. Michael Knyszek’s #35112 replaced the treap-based allocator (mgclarge.go, mheap.go free structures) by a bitmap allocator in mpagealloc.go. Constants and design comment first (14849f0fa5), then the core (39e8cb0faa, 764 lines in mpagealloc.go plus 32/64-bit variants and tests), still unwired. Packed bitmap summaries (cec01395c5) store three integers in a uint64 per summary: free pages at the start, at the end, and the longest free run, summarised at each level of a radix tree over pallocBits; summarisation of the bit sets later became bit-parallel (4e5ed83e8d, up to 95% faster), as did findBitRange64 (8b8f926fc3, ~2x). allocSpan (a762221bea, which merges allocSpanLocked/allocManual/alloc_m) takes the heap lock only where it must; span initialisation became fully lockless, sweeper metadata atomic (dac936a4ab), and zeroing is decided by per-arena zeroedBase (a120cc8b36, 4208dbef16 lock-free), since a free span no longer carries a “needzero” flag. The scavenger was rewritten alongside (see scavenger). Heap growth was changed to be incremental: f18109d7e3 separates mapping an arena (64 MB) from “growing” the heap, tracking curArena and growing in smaller steps (so heap-growth scavenging, needed for #32828, happens at finer granularity). The switch was a single constant flip, oldPageAllocator (e6135c2768, 2019-10-28), after integration (689f6f77f0); 33dfd3529b removed the old allocator. Follow-ups: sparse chunk index replacing a single giant reservation, which failed on systems with low virtual address limits (acf3ff2e8a), inUse ranges owned by the heap (9d78e75a0a, 1b1fbb3192 maps summary memory only as needed), binary search for findSucc for randomised address spaces such as Fuchsia (76bce1dd52), offset address types so scavenger and allocator see a contiguous space on segmented platforms such as AIX (d69509ff99, ea5f9b666c, 796786cd0c, 55ec5182d7), and sanity fixes (d1798d5aa0).
Timeline.
- Go 1.14 — Done: page allocator (
14849f0fa5,39e8cb0faa,73317080e1,cec01395c5,689f6f77f0,e6135c2768,33dfd3529b),allocSpan(a762221bea), incremental growth (f18109d7e3), zeroing (a120cc8b36,4208dbef16), sparse bitmap and inUse ranges (acf3ff2e8a,9d78e75a0a,1b1fbb3192,1475b97090,e7f9e17b79), counting scavenged bits in allocation (e1ddf0507c), memstat atomics (ae4534e659), packaging (7f574e476a), walks (814c5058bb). Left open: address-space-specific work (AIX5042317d69,a3a630b0d2, darwin/arm64198f0452b0). - Go 1.15 — Done: correctness hardening:
allocToCacheupdates searchAddr validly (287d1ec96c),linearAllocandgrowoverflow (2e455ec2eb,14ae846f54), offset addresses (d69509ff99,ea5f9b666c,796786cd0c,dfd613e0e4),pageAlloc.findvalidation (b56791cdea), a barrier after span allocation (c53b2bdb35), DWARFarenaBaseOffsetfor viewcore (12c01f7698). Left open: nothing noted. - Go 1.16 — Done: binary search
findSucc(76bce1dd52), bit-parallel summaries (4e5ed83e8d,8b8f926fc3), zero-size ranges throw (fe70866d1d), chunk iteration fix (34835df048), heap-lock assertions (9393b5bae5) and world-stopped assertions (6abbfc17c2). Left open: nothing noted. - Go 1.17 — Done: arenas prepared incrementally via
sysMapingrowfor strict-overcommit systems (bd6aeca968),pageAllochandlessysAllocfailures (0e8a72b62e). Left open: nothing noted.
Scavenger: returning memory to the OS
Span: Go 1.0 → Go 1.17 · Status at go1.17: a background scavenger paced by its own cost measurements over the page allocator’s address-ordered search with a “free”/“scavenged” watermark; → continues as Scavenger redesign (1.18+)
- design doc Proposal: Smarter Scavenging Michael Knyszek · golang/proposal
- tracking issue #30333 runtime: smarter scavenging referenced by 7 of this thread's commits
- motivating issue #35788 runtime: scavenger not as effective as in previous releases referenced by 3 of this thread's commits
- motivating issue #34048 runtime: scavenger pacing fails to account for fragmentation referenced by 1 of this thread's commits
- motivating issue #32012 runtime: background scavenger is overzealous with small heaps referenced by 1 of this thread's commits
Design. The first scavenger was periodic (5c598d3c9f, 2012-02): a sysmon-driven pass that released free spans unused for 5 minutes (GC forced if none in 2 minutes) with MADV_DONTNEED on Linux and MADV_FREE on BSD. FreeOSMemory made immediate (e893acf184, 1.3). Scavenged spans needed zeroing (9ad236ab72, 1.1); Windows got SysUnused (4e76abbc60). In 1.5 the scavenger was disabled on 64K-page kernels (f9cc72ccfe) and huge page flags changed only at huge page granularity because MADV_NOHUGEPAGE per released region created too many VMAs and “out of memory” failures (44078a3228). 1.7 aligned scavenging to physical pages (2cdcb6f829, 1151473077) and supported smaller physical pages than the compile-time bound (f407ca9288).
1.12 made it aggressive: scavenged spans separated (b46bf0240c), large spans scavenged before heap growth (c803ffc67d), allocate fairly from free and scavenged (064842450b), then when coalescing merged scavenged with unscavenged spans nearly everything became scavenged, increasing page faults and madvise calls; fixed by not coalescing them (6e9f664b9a), treating allocation from scavenged memory as heap growth that scavenges other memory (8e093e7a1c) and a credit pool for over-scavenging (faf187fb8e). 1.13 introduced the background scavenger (fe67ea32bf, 2018-10 authoring date): a goroutine paced to use 1% of mutator time and paced from the number of scavengable huge pages, aimed at a shrinking heap (the goal falls over time); it removed periodic scavenging (eaa1c87b00) and made the scavenger huge-page aware (a62b5723be large spans first, 31c4e09915 sysHugePage for coalesced free spans, 8c3040d768 call it less often, 1033065ee3/a41ebe6e25 physHugePageSize and shift). In 1.14 with the page allocator: scavenge on growth rather than inline (eb96f8a574), a scavenger that walks the heap backwards once per GC (73317080e1), goal defined by heap_inuse scaled by heap goal ratio rather than next_gc because fragmentation can leave no free unscavenged memory (9b30811280), self-paced (21445b091e: previously externally paced by an assumption about the cost of scavenging a page), no work when <1 physical page (08a72c851c), scavtrace debug flag and scavenge info removed from gctrace (8ac98e7b3f), cost approximation that includes sysUsed (71154e061f), woken on sweep done (2491c5fd24), scavAddr replaced by address ranges (55ec5182d7), and 1.15’s free/scavenged high watermarks so that scavenged and untouched memory is not rescanned every cycle (dba1205b2f, citing heap spikes) and defensive pacing (c7915376ce).
Timeline.
- Go 1.0 — Done: user-visible periodic release of unused memory with
madvise(5c598d3c9f),HeapIdletracking (33d00fae30). Left open: fixed 2/5-minute thresholds. - Go 1.1 — Done: scavenged spans marked as needing zeroing (
9ad236ab72). Left open: nothing noted. - Go 1.2 — Done: Windows
SysUnused(4e76abbc60), scavenger details printed onFreeOSMemory(9f46efce52). Left open: nothing noted. - Go 1.3 — Done:
FreeOSMemorysweeps too (e893acf184), Windows decommit failure handling (30b8af98c0). Left open: nothing noted. - Go 1.5 — Done: scavenger disabled on 64K page kernels (
f9cc72ccfe), huge page flags at huge page granularity (44078a32281.6). Left open: nothing noted. - Go 1.7 — Done: physical-page aligned scavenging (
2cdcb6f829,1151473077,f407ca9288). Left open: nothing noted. - Go 1.12 — Done: scavenged/unscavenged separation and credit (
b46bf0240c,e508a5f072,c803ffc67d,06be7cbf3c,064842450b,6e9f664b9a,8e093e7a1c,faf187fb8e). Left open: scavenging still inline with allocation. - Go 1.13 — Done: background scavenger and removal of periodic scavenger (
fe67ea32bf,eaa1c87b00), huge-page-aware (a62b5723be,31c4e09915,8c3040d768,f4a5ae5594,ff70494b75,4e7bef84c1newly mapped memory is marked scavenged because on most OSs untouched pages are not resident). Left open: scavenging pacing relied on estimates; 1.14 made it self-paced. - Go 1.14 — Done: page-allocator scavenger, goal in terms of
heap_inuse, self-paced,scavtrace(73317080e1,9b30811280,21445b091e,08a72c851c,8ac98e7b3f,71154e061f,2491c5fd241.15,eb96f8a574,e5ce13c178,62e4156552,2c87be436b,8a5af7910a,4e3d58009a,d183253572,dcd3b2c173,f511467532) [user-visible:GODEBUG=scavtrace]. Left open: rescans previously-scavenged memory each cycle (fixed 1.15). - Go 1.15 — Done: watermark-based search (
dba1205b2f,55ec5182d7), defensive pacing (c7915376ce). Left open: nothing noted. - Go 1.17 — Done:
scavengePercentmin fraction constant fix (14ade57ab8). Left open: the 1.18 redesign (controller-paced) follows.
Heap address space: contiguous arena to sparse arenas
Span: Go 1.0 → Go 1.17 · Status at go1.17: still the design at go1.17 (64 MB heapArena units on 64-bit, 4 MB on Windows and iOS, two-level arena map on platforms with large address spaces, arenaBaseOffset, heap-arena hints) → continues as Address-space layout, tagged pointers and the mem.go layer (1.18+)
- motivating issue #10460 runtime: 512GB memory limitation referenced by 6 of this thread's commits
Design. In Go 1.0 the 64-bit heap was a contiguous reservation with a flat page-to-span map (“heap map in bss”, 4608feb18b; a multi-level table dropped as overkill because there are only 1M entries on 32-bit, 2011) and on 32-bit a fixed arena (70b0de8e98 allows use of more than 512 MB, d37a8b73c5 falls back to a 32-bit allocator if 64-bit mapping fails). 1.1 allocated heap metadata at run time (8a6ff3ab34), limited allocation to 128 GB (9799a5a4fd); 1.2 made the page table lazily allocated, avoiding a 256 MB startup allocation that conflicted with ulimit (671814b904), made mheap static again (8bbb08533d), and made 64-bit heap mapping more flexible (a8ad859c30); 1.3 fixed 32-bit spans array indexing for pointers ≥ 0x80000000 (8da8b37674). 1.5 raised maxmem to 512 GB (43aac4f9e7), added a 128 MB fallback on 32-bit (85d09574fd), tolerated holes (6d1ebeb527) and initial arena reservation failure (324f38a222, 1d5251f707). 1.7 used the entire 4 GB address space on 32-bit (e6ec82067a, same bitmap size). 1.9 untangled _MaxMem vs _MaxArena32 (4af6b81d41, efb5eae3cf) and tolerated non-monotonic arena allocation on 32-bit (13ae3b3a8d), protecting arena bounds from low mmap (e5a5c03f5b), fall back to small mmaps when reservation cannot grow (780249eed4), and map bitmap and spans at initialisation (623e2c4603).
The sparse heap landed in 1.11 as an incremental series by Austin Clements: abstract all mheap.spans access (0de5324d61, a90f9a00ca, 4de468621a), a sparse heap bitmap (c0392d2e7f) and span map (d6e8218581) in 64 MB chunks reached through an arena index, eliminate most arena_* uses (45ffeab549), then 2b415549b8 “use sparse mappings for the heap”: no limit on heap size (previously 512 GB, fixes #10460), no address-space-conflict panics, which could occur for many reasons. ed1959c6e6 offsets the index by 2^47 on amd64 so solaris/amd64’s “negative” user addresses work; ec25210564 adds a two-level arena map for platforms with full 64-bit address spaces (mips64, ppc64, s390x) and to avoid committing large flat maps on Windows; 788464724c reduces arenas to 4 MB on 64-bit Windows where untouched committed memory is charged (a tiny heap committed 64 MB + 32 MB, and 384 MB under race); 51ae88ee2f removes the non-reserved heap logic. 1.12 tracks all arenas in a slice (e500ffd88c). 1.14: darwin/arm64 33 bits (198f0452b0), AIX arenaBaseOffset (5042317d69, a3a630b0d2), Solaris NORESERVE (947f8504d9), and 1.16 more bits on macOS/ARM64 (a739306ca7). 1.17 prepared arenas incrementally (bd6aeca968) and 4 MiB arenas on iOS (e03cca6407).
Timeline.
- Go 1.0 — Done: dense heap map (
4608feb18b), >512 MB 32-bit (70b0de8e98), alignment checks (48535ae3f1), 64→32-bit fallback (d37a8b73c5,073aeff785,3dcedb620c). Left open: fixed contiguous reservation. - Go 1.1 — Done: run-time metadata allocation (
8a6ff3ab34), 128 GB (9799a5a4fd), noPROT_EXECon heap mmaps (5a8b7dc6d0),runtime.memlimitignored when allocating heap (515353a290). Left open: nothing noted. - Go 1.2 — Done: lazy page table (
671814b904,8bbb08533d,ccd1d07cc4), flexible 64-bit mapping (a8ad859c30),persistentallocuse (99922aba8b). Left open: nothing noted. - Go 1.4 — Done: heap growth by 64K (
0622e13b4d);mincoreinaddrspace_free(0e1b6bb5471.3). Left open: nothing noted. - Go 1.5 — Done: 512 GB, 32-bit fallbacks (
43aac4f9e7,85d09574fd,6d1ebeb527,324f38a222,f9357cdec11.6,0de59c27eb1.6); high-address wbshadow (bc2601a1df). Left open: 512 GB heap limit. - Go 1.7 — Done: full 4 GB on 32-bit (
e6ec82067a). Left open: nothing noted. - Go 1.9 — Done: arena bound fixes (
4af6b81d41,e5a5c03f5b,13ae3b3a8d,780249eed4,623e2c4603,6c6f455f88). Left open: contiguous arena. - Go 1.11 — Done: sparse arenas (
c0392d2e7f,d6e8218581,2b415549b8,ed1959c6e6,ec25210564,788464724c,45ffeab549,51ae88ee2f,e9db7b9dd1), TSAN heap hints (2d4c1ff739), Plan 9 hints (b1dbce31d7,c2cdfbd1a7), race-mode hint exhaustion (4946d9e87b). Left open: nothing noted. - Go 1.12 — Done: arenas slice (
e500ffd88c), race-modeMAP_FIXED(7ef718f16f, from Dec 2018). Left open: nothing noted. - Go 1.13 — Done: wasm memory handling (
460f9c6068optimise memory allocation on wasm,c2d9eea1f1no arena hints on wasm), sbrk alignment (5b15510d96), first persistentalloc chunk aligned (c5babcc485). Left open: nothing noted. - Go 1.14 — Done: platform address-space definitions (
198f0452b0,5042317d69,a3a630b0d2,947f8504d9,763d3ac75c,61ef6a39dd,8054b13536,ffb5646fe0). Left open: nothing noted. - Go 1.16 — Done: macOS/ARM64 more address bits (
a739306ca7), race-detector address (f7e26467b4). Left open: nothing noted. - Go 1.17 — Done: incremental arena preparation, 4 MiB arenas on iOS (
bd6aeca968,e03cca6407). Left open: nothing noted.
Finalizers and span specials
Span: Go 1.0 → Go 1.17 · Status at go1.17: finalizers as special records on spans, scanned as roots concurrently, run by one goroutine; → continues as Finalizers, runtime.AddCleanup and the cleanup queue (1.19+)
Design. SetFinalizer landed in 2010 (33e396a4a7) with a finalizer table, tracked with a per-block flag bit to avoid lookups (f25586a306), table lock (acfd7b8d12), and running all finalizers in one goroutine, eliminating a second mark+sweep pass by scanning the table specially (4e28cfe970, 24c58174b2 explicit flag when finalizer goroutine waits). In 1.3 the table gave way to special records hung off the MSpan for finalizers and heap profile info (020b39c3f3; deallocated before the object e7d010a6a7; locking fixes b039abfc3e for the specialLock/proflock deadlock; ea87501750, b08156cd87, f4ef6977ff ignoring pointers to globals); that also removed the bit bitSpecial from the GC (38f6c3f59d). SetFinalizer accepts func(interface{}) (1.2, 6350e45892) and handles zero-size (4b76a31c6d). Finalizer goroutine moved to Go (c46bcd4d13); the runfinq frame hidden from the GC (e9bbe3a8da, eb04df75cd). Concurrency: marking finalizer-bearing objects moved out of STW (608c1b0d56, 1.6); markrootSpans driven by a per-page bitmap (eacdf76b93, 1.15); finalizers checkmark (2b19b6e3f1); the finalizers queue is not rescanned in mark termination (f1ba75f8c5, 1.9); f009b5b226 supports register-ABI calling of finalizers (1.17). Tiny-allocator objects with finalizers fixed in 1.6 (bf606094ee).
Timeline.
- Go 1.0 — Done: finalizers, table, single goroutine (
33e396a4a7,f25586a306,acfd7b8d12,4e28cfe970,24c58174b2,69a91663d2). Left open: a second mark+sweep pass for finalizers existed until4e28cfe970. - Go 1.1 — Done: special bit not unset after finalization (
348087877c), finalizer root address preserved (e5dae3baaa),runfinqframe hidden from GC (e9bbe3a8da). Left open: nothing noted. - Go 1.2 — Done: GC and finalizer goroutine synchronised (
72c4ee1a9d),SetFinalizer(func(interface{}))(6350e45892), no finalizers on g0 (6401e0f83f), frame in runfinq hidden (eb04df75cd). Left open: nothing noted. - Go 1.3 — Done: specials (
020b39c3f3,e7d010a6a7,b039abfc3e), finalizer flakiness fix (241f63debd), leak fix in runfinq (b08156cd87), globals ignored (f4ef6977ff). Left open: nothing noted. - Go 1.4 — Done: finalizer thread in Go (
c46bcd4d13). Left open: nothing noted. - Go 1.6 — Done: concurrent scan of finalizer objects (
608c1b0d56), tiny-alloc finalization fix (bf606094ee). Left open: nothing noted. - Go 1.8 — Done: zero-initialised finalizer frames (
d3836aba31,db56a63547), weakerSetFinalizerpanic claim (99339dd445). Left open: nothing noted. - Go 1.15 — Done: bitmap-based
markrootSpans(eacdf76b93). Left open: nothing noted. - Go 1.17 — Done: register-ABI finalizers (
f009b5b226), abstracted specials list iteration (1ef114d12c). Left open: nothing noted.
Mark work distribution: parfor, work buffers, gcWork and root jobs
Span: Go 1.1 → Go 1.17 · Status at go1.17: still the design at go1.17 (per-P gcWork with two cached work buffers, global full/empty lists, markroot jobs for data/BSS/span/stack roots shared by workers and assists) → continues as Mark worker scheduling and GC work queues (1.19+)
Design. 1.1’s parallel GC used parfor (95643647ae) to divide roots and used a shared work pool; d4c80d19a8 gave each thread its own work buffers, eliminating contention on the global mutex-protected pool, and commits db7fd1c142 and a15818fed3 in 1.5 shrank buffers to 256 B so that per-P caches could not hide much work from mark termination. 1.4 cached one workbuf in thread-local storage (f6f2f77142) because scanblock is called for many small root pieces, such as each stack. In 1.5 Austin Clements introduced the gcWork abstraction (b30d19de59, adopted by 02dcdba7c8, cadd4f81a8 merges producer into gcWork) in gcwork.go (277d5870a0), with a producer/consumer interface, gcDrain and gcDrainN in terms of scan work (028f972847), and per-P instead of per-M caches (1b4025f4bd). 1.6 enlarged work buffers (1870572180), cached two buffers to avoid flapping at the boundary (b6c0934a9b) and balanced eagerly (9439fa1078), removed work.partial (feb92a8e8c) and the getfull barrier (62ba520b23, ee3d20129a in 1.8 to avoid it most of the time). Roots: markroot jobs for span roots split into 128 subtasks (572f08a064), data/BSS in 256K chunks (d3df04cd8c), stack scanning per goroutine; assists can perform root jobs (d35dfd405e, 1.8) so goroutines do not block at cycle start; idle workers exit when higher-priority work arrives (49ea9207b6) and idle Ps are woken when work is enqueued (0bae74e8c9). In 1.9 workbufs are allocated from manually-managed spans (9cc883a466) and freed in sweep (051809e352); in 1.14 root bases are precomputed in gcMarkRootPrepare (82e4a6310b in 1.17); 1.8 a475a38a3d parallelises STW mcache flushing; scan work bounded to ~100 µs per scanobject chunk (cf4f1d07a1) because non-preemptible large-object scans delayed assists and the scheduler on that P. 1.12 added checkPut debugging for late gcWork puts (9098d1d854, bae1e70ac4, 438b9544a0).
Timeline.
- Go 1.1 — Done: parfor and per-thread work buffers (
95643647ae,d4c80d19a8),addrootoptimisation (ab08eac5b7), strings marked without a buffer (ec8caf696a). Left open: nothing noted. - Go 1.4 — Done: parfor converted to Go (
98bebcc90a), workbuf cache (f6f2f77142,ac9218f5f0), parfor untyped allocation removed (fc46931442). Left open: nothing noted. - Go 1.5 — Done: gcWork (
b30d19de59,02dcdba7c8,277d5870a0,c2de2f87f0,cf964e1653,1ac65f82ad,15c9a2ef4e,60a16ea367,1b205857a4,a15818fed3caches workbufs on Ms), per-P cache (1b4025f4bd),parformoved to its own file (6b7b0f9a0c,8e2bb7bb4a,ebbdf2a14c,428afae027), work buffer tracing off (5b66e5d0d8). Left open: small buffers. - Go 1.6 — Done: root jobs and buffers (
572f08a064,d3df04cd8c,1870572180,b6c0934a9b,9439fa1078,feb92a8e8c,62ba520b23,dcd9e5bc0f), mark workers attached to P after parking (eb3b1830b0). Left open: nothing noted. - Go 1.7 — Done: only per-P gcWork (
0168c2676f,7b229001e7),gcWorktryGet/put fast paths (1354b32cd7), scanstack takes the gcWork (3be48b4dc8). Left open: nothing noted. - Go 1.8 — Done: bounded
scanobject(cf4f1d07a1), assists perform root jobs (d35dfd405e), idle worker exit and wakeup (49ea9207b6,0bae74e8c9,dd500193d3,d70b0fe6c4),getfullavoided (ee3d20129a), STW mcache flush parallelised (a475a38a3d). Left open: nothing noted. - Go 1.9 — Done: manually managed workbuf spans (
9cc883a466,051809e352), lfstack type (13ae271d5d),489620d878(drain local runq when a dedicated worker runs). Left open: nothing noted. - Go 1.12 — Done: work-flush debugging (
9098d1d854,438b9544a0,bae1e70ac4,8e6396115e). Left open: nothing noted. - Go 1.17 — Done: root bases computed in
gcMarkRootPrepare(82e4a6310b),nFlushCacheRootsremoved (94817890c2). Left open: nothing noted.
Runtime allocators for runtime metadata and the port to Go
Span: Go 1.2 → Go 1.9 · Status at go1.17: persistentalloc, fixalloc and manually-managed spans are still the metadata allocators; the allocator and collector are Go code since 1.5
Design. persistentalloc (47e0a3d7b1, 86da989ee5, Go 1.2) is a caching wrapper around SysAlloc that serves small chunks, important on Windows where VM is reserved in multi-page units; it replaced direct SysAlloc in the GC (99922aba8b) and FixAlloc (4f514e8691, FixAlloc granularity 128k→16k). 1.2 also stopped the GC from seeing mheap itself (2a6520c2d3). 1.3 added the allocation-heavy facility sync.Pool (8c6ef061e3, initial implementation, with 8fc6ed4c89 in 1.3 limiting local caching to one element per P after a case where a Pool of 2 MB objects could waste 960 MB at GOMAXPROCS=32). 1.4: 7c13860cd0 separates the stack allocator from mallocgc, because if malloc runs out of stack it cannot allocate a new one, and stacks were the last remaining FlagNoGC objects in the heap; it enables removing the allocated/blockboundary distinction. 1e2d2f0947 (dev.cc, 1.5) converts the memory allocator and GC to Go by automated tool; a0862a175d converts mem_windows; b90638e1de deletes old .h files. ae3bb4a537 (1.8) makes fixalloc zero on reuse (it was “the only allocator that doesn’t zero on allocation”; important for the hybrid barrier), and 3d1f8c2379/b07f60b97f (1.5/1.12) tidy OS-specific memory code. 1.5 484f801ff4 reorganises memory code into mem_*.go, and 6f2c0f1585 adds a check for malloc in a signal handler. 1.9 df6025bc0d disallows malloc or panic in the scavenger.
Timeline.
- Go 1.2 — Done: persistentalloc, FixAlloc use (
47e0a3d7b1,86da989ee5,99922aba8b,4f514e8691),mheaphidden from GC (2a6520c2d3), all sys memory accounted (a33ef8d11b, see stats). Left open: nothing noted. - Go 1.4 — Done: stack allocator (
7c13860cd0), stack orders raised to 4 in 1.5 (5aae246f1e1.5),sysAllocnosplit (99a08262af). Left open: C-written malloc. - Go 1.5 — Done: malloc and GC in Go (
1e2d2f0947,8420df622a),runtimememory layout reorganised (484f801ff4,6c4b54f409), allocator on Plan 9 fixed with a K&R-style allocator with coalescing and splitting, no header on blocks because size is always passed tosysFree(fb75f856df,3c3848ad92,27ed1fcb04,c84ae1c4991.6). Left open: nothing noted. - Go 1.8 — Done: fixalloc zeroing (
ae3bb4a537),go:notinheaptypes (1bc6be6423,991a85c889). Left open: nothing noted. - Go 1.9 — Done: manually managed spans for workbufs and stacks (
9cc883a466,9741f0275c,dc0f0ab70f), malloc forbidden in scavenge (df6025bc0d). Left open: nothing noted.
Memory statistics and accounting
Span: Go 1.0 → Go 1.17 · Status at go1.17: sharded consistentHeapStats and per-mcache stats feed MemStats, GC pacer state lives in gcController
- design doc Proposal: API for unstable runtime metrics Michael Knyszek · listed in Go: A Documentary
- design doc Go runtime stats
- motivating issue #16843 runtime: mechanism for monitoring heap size listed in Go: A Documentary
Design. Early stats were global counters under the heap lock; 66d5c9b1e9 made per-M MemStats caches to avoid touching centralised state. 1.1: cea46387b9 adds GC statistics, fd32ac4bae splits stop-the-world time in GOGCTRACE. 1.2: a33ef8d11b accounts for all sys memory: GC bitmap, spans table, GC roots blocks, finalizer blocks, iface table and netpoll descriptors (up to about 20% was unaccounted) with new GCSys and OtherSys stats user-visible, every sys allocation function now takes the stat so no allocation can be missed; 5d637b83a9 counts only frees on the hot path. 1.4 added PauseEnd user-visible (46af78915a), e19d8a47d1 counts tiny allocs. 1.5 added GCCPUFraction (23e4744c07) and gctrace lines show start-of-cycle time and heap sizes. 1.8 NumForcedGC (01c6a19e04). 1.9 made ReadMemStats stop the world for under 25 µs (4a7cf960c3). The 1.16 series by Michael Knyszek makes malloc stats distributed and consistent without stopping the world: 42019613df makes mcache-local stats the source of truth (a dying mcache donates its stats to P 0’s), fe7ff71185 adds consistentHeapStats using a sequence counter and a generation counter so writers (allocators) synchronise minimally and readers get a consistent snapshot, f77a9025f1 and 79781e8dd3 move stacks, workbuf and malloc stats into it (tiny alloc counts stay inconsistent and are flushed periodically because moving them cost 50% throughput on one core), ad863ba32a breaks down gc_sys, 8ebc58452a marks system stats with a type, 39a5ee52b9 allows P-less updates. In 1.17, f2d5bd1ad3 moves internal pacing stats (gc_trigger, triggerRatio, heap_marked) from memstats to gcController.
Timeline.
- Go 1.0 — Done: per-M caches (
66d5c9b1e9),HeapIdle(33d00fae30),ReadMemStats(842c906e2e), mallocs and sampling fixes (ad35cea762,1135fc3978). Left open: nothing noted. - Go 1.1 — Done: per-M GC stats (
d839a809b2),cea46387b9, uintptr malloc stats (0b09425b5c,09f48db3e1),memorydump()(e0c9d04aec). Left open: nothing noted. - Go 1.2 — Done: user-visible
GCSys,OtherSys, all sys memory accounted (a33ef8d11b), cheaper stats (5d637b83a9,c075d82cca). Left open: nothing noted. - Go 1.3 — Done: user-visible
WriteHeapDump(fff63c2448), runaway-memory skew fixed (aea99eda0f), gctrace clearer (5539ef02b6). Left open: nothing noted. - Go 1.4 — Done:
PauseEnd(46af78915a), stats fixes (30ef2c7deb,266d350f5e,01a7d39df1), tiny allocs counted (e19d8a47d1),gctracegoroutine count (ed7db89b90). Left open: nothing noted. - Go 1.5 — Done: user-visible
GCCPUFraction(23e4744c07),PauseEndin UNIX time (ad60cd8b92,777ab5ce1a), gctrace format (babeb4a963,cc6a7fce53,10083d8007). Left open: nothing noted. - Go 1.8 — Done: user-visible
NumForcedGC(01c6a19e04). Left open: nothing noted. - Go 1.9 — Done: user-visible
ReadMemStatsSTW under 25 µs (4a7cf960c3). Left open: nothing noted. - Go 1.14 — Done: atomic heap memstat updates (
ae4534e659), uselessheap_objectsaccounting removed (814c5058bb). Left open: nothing noted. - Go 1.16 — Done: sharded and consistent stats (
42019613df,e63716bc76,a5088e76f1,cca3d1e553,d677899e90,dc02578ac8,8ebc58452a,ad863ba32a,c02134abb0,fe7ff71185,f77a9025f1,79781e8dd3,39a5ee52b9,ac766e3718). Left open:tinyAllocsremain inconsistent by design. - Go 1.17 — Done: internal pacing stats moved into
gcController(f2d5bd1ad3). Left open: nothing noted.
Allocation-site profiling and GC debugging modes
Span: Go 1.0 → Go 1.17 · Status at go1.17: gccheckmark, clobberfree, zombie-slot detection, scavtrace, allocfreetrace, invalidptr and cgocheck are all present
Design. The runtime accumulated debug modes tied to each GC redesign. 1.3: GODEBUG=allocfreetrace=1 (stack traces for each alloc/free/GC, 48279bd567, 3ec60c253d, adjusted 1ec4d5e9e7 with gcdead=1 to poison freed memory) and efence-like marking of free blocks (c115cda22c). 1.4 WriteHeapDump follow-ups; 1.5: wbshadow (dcec123a49, d94192180f, bc2601a1df) found missing write barriers using a shadow heap and was removed once gccheckmark (9626561030) was enabled by default because the latter needs no extra memory (1635ab7dfe); d5b40b6ac2 adds gcshrinkstackoff, gcstackbarrieroff, gcstoptheworld; 1b0b0db6fb adds profiling infrastructure for the concurrent GC; 38d9b2a3a9 diagnoses invalid pointers during GC (invalidptr, default 1 by d3ffc975f3); 5250279eb9 detects corrupted free lists. 1.6: be1ef46775 cgocheck=2. 1.7–1.9: d1cc83472d panic when marking a free object. 1.13: af8f4062c2 introduces GODEBUG=clobberfree which clobbers memory when GC frees an object. 1.15: precise zombie slot detection (ea2de3346f). 1.17: 0ccdcb2102 crash the GC if a clobberdead pointer is seen. Memory profiling: 596c16e045 (1.0), nextSample fixes, eee85fc5a1/1be3e76e76 heap profile snapshot in mark termination (1.9), dc889025c7 large allocation sampling (1.12), 01d137262a uintptr sample counter (1.13), 40f0359d52 skip unneeded work when MemProfileRate==1 (1.16), fdb45acd1f sampling moved into the m-acquired section (1.17).
Timeline.
- Go 1.3 — Done:
allocfreetrace(48279bd567,3ec60c253d,1ec4d5e9e7), efence improvements (c115cda22c,da1bea0ef0). Left open: nothing noted. - Go 1.5 — Done:
wbshadow,gccheckmarkdefault,invalidptr(dcec123a49,9626561030,d3ffc975f3,38d9b2a3a9,5250279eb9,1635ab7dfe). Left open: nothing noted. - Go 1.6 — Done:
cgocheck=2(be1ef46775) user-visible. Left open: nothing noted. - Go 1.8 — Done: freed-object marking check (
d1cc83472d), improved scan diagnostics (05dc6b26ca,38f1df66ff). Left open: nothing noted. - Go 1.13 — Done:
clobberfree(af8f4062c2). Left open: nothing noted. - Go 1.15 — Done: zombie slot report (
ea2de3346f). Left open: nothing noted. - Go 1.17 — Done:
clobberdeadcheck in GC (0ccdcb2102), GC testing helpers (4e1bf8ed38). Left open: nothing noted.
Changes in this slice that are not part of a thread are listed on the release pages: 1.0 · 1.1 · 1.2 · 1.3 · 1.4 · 1.5 · 1.6 · 1.7 · 1.8 · 1.9 · 1.10 · 1.11 · 1.12 · 1.13 · 1.14 · 1.15 · 1.16 · 1.17
Runtime: memory allocator and garbage collector (Go 1.18–1.28)
Reader: Claude Opus. Cites 410 of 463 commits in this area; the 11 non-noise commits left uncited are typo fixes and small cleanups. Mechanisms were checked against diffs where messages were unclear.
GC pacer redesign
Span: Go 1.18 → Go 1.21 · Status at HEAD: redesigned pacer is the only pacer since 1.19 (PacerRedesign GOEXPERIMENT added off in 1.18, defaulted on in 1.18, removed in 1.19); the 512 KiB heap minimum it introduced still sits behind the off-by-default HeapMinimum512KiB experiment
Earlier history: GC pacer: trigger controller, assists and background utilisation (Go 1.5 → Go 1.17)
- proposal #44167 proposal: runtime: GC pacer redesign referenced by 10 of this thread's commits
- design doc GC Pacer Redesign Michael Knyszek · linked from #44167
- tracking issue #42430 runtime: GC pacer problems meta-issue listed in Go: A Documentary
Design. The pacer (gcControllerState in mgcpacer.go) predicts when to start a cycle so that marking finishes at the heap goal using only the 25% background workers (gcBackgroundUtilization, and at HEAD gcGoalUtilization equals it). The redesign (proposal #44167) changed two things. First, the heap goal now counts all scan work, not just heap: gcPercentHeapGoal = heapMarked + (heapMarked + lastStackScan + globalsScan) * GOGC/100. Scannable stack is tracked per P and flushed in 8 KiB deltas (9ac1ee2d46), and scannable globals are counted at module load (9da64156a6). Stack and globals scan work also feed the assist credit system. Second, the trigger comes from a single steady-state model: the measured cons/mark ratio, meaning bytes allocated per unit of scan work, normalised by utilization. commit turns it into a runway, consMark * (1-u)/u * (lastHeapScan + lastStackScan + globalsScan), and the trigger is the goal minus that runway, clamped between ~0.7 (minTriggerRatioNum = 45/64) and ~0.95 (maxTriggerRatioNum = 61/64) of the distance from heapMarked to the goal. The minimum bound was raised from the design’s 0.6 to 0.7 because the new pacer “much more consistently hits the goal … leading to slightly less frequent GC cycles, but longer ones”, and tail latency improved by up to 10% (a108b280bc).
Smoothing of cons/mark turned out to be the hard part. The 1.18 pacer ran it through a PI controller. In 1.19, switching to the actual trigger point instead of the precomputed one exploited the controller’s long memory and caused a memory regression (#53738), so the cycle shipped with the precomputed trigger (85a482fc24). In 1.20 the PI controller was replaced by a two-cycle moving average together with the actual trigger (41795528b0). In 1.21 it became a deliberately biased estimator: the maximum cons/mark over the last 5 cycles (lastConsMark [4]float64 plus the current one). This trades slightly earlier, more frequent GCs for fewer assists, and “real production applications showed a reduction in tail latencies of up to 45%” (b513bd808f).
Timeline.
- Go 1.18 — Done: user-visible Pacer state was untangled from global state so it could be unit-tested: atomic
gcPercent(6508fdad9d), scavenger pacing (f99e40aac0), sweeper pacing (413672fc84),startCycle/endCycle/heapLivemethods (4a5d78ff8b,8e112a7c2a,5ec2427357), atomic types (75b73d68b3,2f71c86370,09e8de70c2), and a simulation test framework driven by the real pacer (994049a9ad,b5a5b7bfb1). The redesign landed behindGOEXPERIMENT=pacerredesign. Its message says “on by default”, but the commit does not touchinternal/buildcfg(a108b280bc); a separate commit turned it on (961aab26bf). The minimum heap was cut to 512 KiB on the basis of ~200 KiB of runtime globals. That was pulled back into its own off-by-defaultheapminimum512kibexperiment after ~1% build slowdowns and “up to 8x” more GCs for <4 MiB heaps (c5c1955077). Fixes: hard-goal computation (156abe5122); the PI controller made robust to overflow/NaN (e4a173adf6); exact float arithmetic for GOGC=25 (8f374aa27f). Left open: the hard-goal case “is impossible [to test] in the idealized model … leave a TODO” (156abe5122). The heap-minimum question was deferred to “next cycle” (c5c1955077). biogo-igor regressed because the write barrier stays on longer (a108b280bc). - Go 1.19 — Done: user-visible the old pacer and the experiment were deleted (
3334afd760). To support the memory limit, the goal and trigger are now computed on demand from current inputs, with a newtriggeredfield recording where a cycle actually started (129dcb7226). The maximum-trigger computation was rewritten to match its intent (473c99643f), and trigger math moved to integer shifts (973dcbb87c). gctrace now prints the correct goal (a6c75aa5c3). A late regression came from making alloc counts truly monotonic (79db59ded9), which also madeheapLivemonotonic and caused small-heap memory growth; it was partially reverted to a “non-monotonic overestimate” (1ebc983000), and the pacer went back to the precomputed trigger (85a482fc24). Left open: “in the future we should consider moving away from such a history-sensitive smoothing function” (85a482fc24). - Go 1.20 — Done: the PI controller was replaced with a 2-cycle moving average using the actual trigger; the PI controller moved to
mgcscavenge.go, its only remaining user (41795528b0). gctrace printslastStackScaninstead of the max (56bdf7f7d9). Pacer fields were converted to atomic types (see Other, Go 1.20). Left open: right after a phase change the average lags, which can cause a transient rise in assists (tile38 WithinCircle,41795528b0). - Go 1.21 — Done: cons/mark smoothing became the max over 5 cycles, biased against assists (
b513bd808f, comment fix600b982df9). Left open: after an application becomes less GC-active, the stale cons/mark persists for 5 cycles. The commit accepts this (b513bd808f).
Soft memory limit and GC CPU limiter
Span: Go 1.19 → Go 1.25 · Status at HEAD: GOMEMLIMIT/debug.SetMemoryLimit and the always-on GC CPU limiter shipped in 1.19; the feature flag was removed in 1.21; accounting fixes ran through 1.25
Paths not taken: Heap limits before GOMEMLIMIT: SetMaxHeap, GC backpressure, a memory target (superseded)
- proposal #48409 runtime/debug: soft memory limit referenced by 11 of this thread's commits
- design doc Proposal: Soft memory limit Michael Knyszek · linked from #48409
- proposal #44309 proposal: runtime/debug: user-configurable memory target linked from #48409
- design doc User-configurable memory target Michael Knyszek · listed in Go: A Documentary
- background Memory Limits for Go (Public)
Design. The memory limit is a second heap goal that is compared with the GOGC goal; the lower one wins (heapGoalInternal). memoryLimitHeapGoal starts from the limit and subtracts everything that counts toward it but is not heap objects: nonHeap = mappedReady − heapFree − heapAlloc, plus any current overage. It then keeps a headroom of 3% (memoryLimitHeapGoalHeadroomPercent), with a fixed minimum, and never returns less than heapMarked. The inputs are new, orthogonal accounting: mappedReady, all memory in the Ready state, tracked inside the sysMap/sysUsed/sysFree wrappers in mem.go (4649a43903, f990b0f1e8); heapFree, which replaced the non-orthogonal heap_sys (d36d5bd3c1); and direct totalAlloc/totalFree counters (85d466493d). Because non-heap memory such as goroutine stacks can change between cycles, the goal and trigger are recomputed on each check, and allocManual (stacks, workbufs) can itself trigger a GC (7e4bc74119). The allocator and the scavenger also enforce the limit. The background scavenger keeps a second goal 5% below the limit (reduceExtraPercent), and span allocation runs a synchronous scavenge assist when the next allocation would exceed the limit. That assist time is charged as GC CPU (b4d81147d8).
The GC CPU limiter (mgclimit.go) prevents the death spiral a limit can cause. It is a leaky bucket that fills with GC CPU time (mark- and scavenge-assist time plus the 25% background-worker share while the GC is on) and drains with mutator time, so it caps GC at roughly 50%. Capacity is capacityPerProc = 1e9 ns × GOMAXPROCS, and fill never goes negative, so idle periods earn no credit. While the bucket overflows, assists are disabled and in-flight mark and scavenge assists are cancelled (1f0ef6bec7). The GC may then overshoot the goal. Idle time is subtracted from mutator time, first idle-mark-worker time (08d6c4c2b9) and then all P idle time via pidleget/pidleput (54bd44e573). This lets the limiter engage on over-provisioned machines. In-flight events are read through a per-P packed (3-bit type, 61-bit timestamp) word, so each update only counts CPU time within its own window (d7941030c9).
Timeline.
- Go 1.19 — Done: user-visible the limiter was added and turned on unconditionally, along with a feature flag for memory-limit code (
01359b4681). Then came, in order: limit plumbing (986a31053d),mappedReady(4649a43903),sys*wrappers (f990b0f1e8), alloc/free counts (85d466493d),heapFree(d36d5bd3c1), stats moved intogcController(375d696ddf), the dynamic goal (129dcb7226), the limit-derived goal (7e4bc74119), allocator and scavenger enforcement (b4d81147d8), the exportedruntime/debug.SetMemoryLimit(f01c20bf2b), and theGOMEMLIMITparser accepting B/KiB/MiB/GiB/TiB (0feebe6eb5). Limiter refinements: a private assist-time pool (2eb8b6eec6), idle-mark and total-idle accounting (08d6c4c2b9,54bd44e573), cancelling assists (1f0ef6bec7), windowed accounting (d7941030c9), and a/gc/limiter/last-enabledmetric (cfccb5cb7c). Left open: the 61-bit timestamp can produce “hiccups” when the top bits change (d7941030c9). Thrashing with idle mark workers disabled (periodic GC) is not handled (08d6c4c2b9). The fixed 1 MiB headroom later caused eager scavenging (#57069). - Go 1.21 — Done: user-visible the limit is initialized in
mallocinit, so early allocations do not scavenge needlessly (96e8e62821). Headroom became proportional (3%) to stop GOGC=off programs from scavenging on most allocations (e97bd776f9). Eager-scavenge details were added toscavtrace(a3e90dc377). Thego119MemoryLimitSupportflag was removed (79edd1d19d), a lost idle-time flush was fixed (24617a068c), and a/gc/gomemlimit:bytesmetric was added (2544b1051e). Left open: “This choice is still somewhat arbitrary … perhaps worth revisiting” (e97bd776f9). - Go 1.22 — Done:
gcPaceScavenger’s memory-limit goal was corrected to(100−reduceExtraPercent)/100 × limit(729f214e3a). Left open: nothing noted. - Go 1.23 — Done: GC CPU pause accounting became exact. The pause update was factored out (
08a7ab97ba) and usesmaxprocsinstead ofstwprocs(ae0a08dee9). Pause time now starts when the world is actually stopped, removing overlap with idle time (d4395ebc0c), and the time Ps spend in_Pgcstopwhile stopping is added back individually (e995aa95cb). The limiter consumes these values. Left open: nothing noted. - Go 1.25 — Done:
sched.idleTimewas double-counted when the limiter forced an up-to-date flush; fixed (385000b004). Left open: nothing noted.
Green Tea: span-based marking with vector scan kernels
Span: Go 1.25 → Go 1.28 · Status at HEAD: GOEXPERIMENT=greenteagc added off in 1.25, default on in 1.26 (loong64 included later in 1.26); flag and the mgcmark_nogreenteagc.go fallback still present; AVX-512 kernels only on amd64
Paths not taken: Non-moving generational GC with card marking (never landed)
- tracking issue #73581 runtime: green tea garbage collector referenced by 12 of this thread's commits
- background Michael Knyszek and Austin Clements. The Green Tea Garbage Collector. October 29, 2025. (experimental in Go 1.25, default in Go 1.26) listed in Go: A Documentary
Design. The classic mark loop greys one object at a time, so access is random and each small object costs at least one cache-line miss plus a separate metadata lookup. Green Tea (1b40dbce1a) delays marking for small-object spans (single 8 KiB page spans whose objects keep heap bits in the span, i.e. objects ≥16 bytes up to 512 bytes; see gcUsesSpanInlineMarkBits) and scans them in batches. Each such span ends in a spanInlineMarkBits block: 63 bytes of marks (“grey”), 63 bytes of scans (“black”), a spanScanOwnership byte and the spanClass. That is enough to scan without touching the mspan. When tryDeferToSpanScan finds a pointer into such a span (checked through a per-arena pageUseSpanInlineMarkBits bitmap), it sets the mark bit atomically. If the object is noscan it is done. Otherwise it tries to take span ownership and enqueues one objptr (span base + object index) for the whole span. scanSpan releases ownership. If only one mark arrived (spanScanOneMark) it scans that single object; otherwise it diffs marks against scans into an ObjMask. With fewer than nelems/8 marked objects it scans sparsely (scanObjectsSmall). With higher density and hardware support it calls scan.ScanSpanPacked, which expands the span’s pointer bitmap by the object mask and writes dereferenced pointers to gcw.ptrBuf in one pass. Before sweeping, inline marks are OR-merged into gcmarkBits (moveInlineMarks).
The kernels live in internal/runtime/gc/scan, imported from Austin Clements’ “gclab” prototype (889ab74169). They need AVX512 VL/BW/DQ/BITALG/VBMI plus GFNI and are generated by mkasm.go. Work distribution changed from the original design: the first version had a fixed-size local span queue spilling half into a locked global span list (1b40dbce1a). In 1.26 this became unbounded per-P spanSPMC rings, which owner and thieves both pop from the tail so objects keep accumulating on queued spans. A work.spanqMask P bitmask is used for stealing and mark-termination checks, and the global queue was removed (d7a38adf4c).
Timeline.
- Go 1.25 — Done: user-visible the
greenteagcGOEXPERIMENT was added, off by default (f1ebad19bd). Size-class tables moved intointernal/runtime/gcso generators could share them (528bafa049,e90ba1d208). The core landed: scan bits, per-P span queue, global overflow list, and per-size-class stats underGODEBUG=gctrace=2(1b40dbce1a). Supporting work: a faster inner-loop bit clear (b30fa1bcc4); settingmspan.limitbefore publication so the conservative scanner cannot index pastallocBits(4c7567290c); allocation-time publication made safe against conservative scanning (24ea1aa25c, see the publication thread); per-P overhead cut from ~2.6 KiB to 536 bytes when the experiment is off (d4bf716793); an explicit nil check inspanInlineMarkBits.initto avoid read-faulting fresh pages to the zero page, which made hugo slower (f8ccda2e05); and word-at-a-time mark merging, ~33% faster, with clearing folded in and skipped for fresh spans (397d2117ec,0c6296ab12,6d4a91c7a5,75a19dbcd7). Left open: “A future extension … is to use SIMD-accelerated scanning kernels” (1b40dbce1a); experiment off by default. - Go 1.26 — Done: user-visible enabled by default except on loong64, where the compiler crashed on the Green Tea code (
7fbf54bfeb); loong64 enabled later in the cycle (c53cb642de). The AVX-512 kernels were imported (889ab74169) and wired intoscanSpan, giving “an extra 15-20%” on dense spans (5d040df092).scanobjectwas duplicated per mode to tell them apart in profiles (41b429881a,1aa154621d). The global span queue was replaced byspanSPMCrings and work-stealing (d7a38adf4c);gcMarkWorkAvailablewas split into a termination condition and a worker-spawn heuristic (fde10c4ce7). Follow-up fixes forspanSPMC: an unbounded loop when more than 64 live rings existed (f3312124c2), rings leaked on P destroy (3ee761739b,7dd54e1fd7), list type (046dce0e54), lock rank (052c192a4c), and removal of the unusedmspan.scanIdx(2baa1d1762). Kernel fixes and tuning: cache-size probing (adbf59525c), a size-class check (ff2ebf69c4), a register destination forVPCOMPRESSQ(−70% on Zen 4,041f564b3e), an AVX-512filterNil(+40% throughput,a9704f89ea), andVZEROUPPER(0d71bd57c9). A Go-simd-package kernel set (da92168ec8, −19% ns on the scan benchmark) was reverted for naming and merge conflicts with a plan to “do it again” (0c69e77343). Left open:freeSomeSpanSPMCsbatching was dropped, “may want to revisit … for very high GOMAXPROCS” (f3312124c2). The simd-package kernels have not been relanded at HEAD, andscan_go.gostill carries a TODO. - Go 1.27 — Done: kernel hardening: symbol names (
d8d2b90a46), retpoline-safe assembly (8572b1cfea), requiring POPCNT for GOAMD64=v1 (45929774c3), and requiring AVX512DQ forKMOVB(661aadb7c4). Left open: nothing noted. - Go 1.28 — Done: the benchmark now cycles random object masks (
27f1d81f7d). Left open: experiment flag not yet removed.
Heap bitmap rewrite and allocation headers
Span: Go 1.20 → Go 1.27 · Status at HEAD: allocation headers are the only heap-metadata scheme (experiment added and defaulted in 1.22, removed in 1.23); GC programs removed in 1.24
Earlier history: Precise heap scanning: type information, the heap bitmap and GC programs (Go 1.1 → Go 1.17)
Design. Up to 1.19 every heap word had 2 bits in an arena-wide bitmap (pointer + “no more pointers”), and type information was unrolled into it at allocation time. Go 1.20 rewrote this to 1 bit per word with word-sized operations and a separate no-more-pointers bitmap (6a9c674a09). Go 1.22 replaced the global bitmap altogether (38ac7c41aa). Objects ≤512 bytes (minSizeForMallocHeader) keep a packed 1-bit-per-word bitmap at the end of their span, written by heapSetTypeNoHeader. Larger small objects carry an 8-byte malloc header holding the *_type. Objects with their own span store the type in mspan.largeType. Pointer/scalar data for header objects is “tiled” from typ.GCData on demand by the typePointers iterator, so nothing is written at allocation and arrays reuse the element mask. The commit cites “a flat 1.5% memory use reduction”. The header sits at the object’s start. Footers (1e250a2199) briefly preserved the implicit 16-byte alignment, but caused an extra cache miss hidden by large L3s in benchmarking, so headers were restored (0ef169abb1). Once types no longer needed unrolling into a heap-wide bitmap, GC programs were removed: the runtime builds large-type bitmaps on demand (45869f5931), and the compiler keeps huge objects out of stack frames (5a0f2a7a7c).
Timeline.
- Go 1.20 — Done: 1-bit bitmap rewrite (
b589208c8c). It was reverted for wasm/386 failures (ad0287f496) and relanded unchanged (6a9c674a09) after making the GC never scan noscan objects (cb13022a24) and initializing pointer bits for noscan spans for cgo/heapdump callers (a6e6b11e3a). Word-at-a-time bitmap processing (“~30% faster in heapBitsSetType”) landed, was reverted and relanded the same way (c3833a5543,e1a8e0e05d,e49e876455). Left open: “we should make the gcdata bitmap an array of uintptr … Similarly for the processing of gc programs” (6a9c674a09). - Go 1.22 — Done: user-visible preparation (
7a606fef66,cdf1362179,25867485a7), theallocheadersimplementation (38ac7c41aa), and default enablement (e3585c6757). Benchmarks with footers: GoBuild −3–4%, tile38 −1.9% (1e250a2199). Headers were restored after a persistent dashboard regression (0ef169abb1). Follow-ups:typePointersOfTypefast path forbulkBarrierPreWrite, removing a p90 regression (d6ef98b8fa); memory-profile accounting using the fullelemsize(f67b2d8f0b,c418da34cd); arenalargeTypeclearing (052d402985); unrolled-bitmap flag (ee4b61a32e) and freeing leak (0f3876f8c1); smallermspan.freeindex/nelemsfields (340a4f55c4); test fixes (17eb0a2bac,b71d43296f1.21). Left open: objects may lose incidental 16-byte alignment; “A follow-up change will add a GODEBUG flag that ensures 16 byte alignment” (0ef169abb1). No such GODEBUG exists ininternal/godebugsat HEAD. - Go 1.23 — Done: user-visible the experiment and old code were removed (
9f3f4c64db). Headers made large pointerful zeroing preemptible (de3a3c9ebc, publication thread). Left open: nothing noted. - Go 1.24 — Done: GC programs removed from types (
45869f5931) and stack objects (5a0f2a7a7c). Small-object bitmap writes were sped up: skippingspan.heapBits(−8%,56fb8350c8) and pre-writing the single possible 8-byte pattern (−6%,721c04ae4e). Left open: nothing noted. - Go 1.26 — Done: invariant work hoisted out of
heapBitsSmallForAddrInlinein Green Tea’s sparse scan loop (65c09eafdf). Left open: nothing noted. - Go 1.27 — Done: the small all-pointer-array fast path computed scan size as only the first pointer; fixed (
40a10063e8). Left open: nothing noted.
mallocgc decomposition and size-specialized malloc
Span: Go 1.24 → Go 1.28 · Status at HEAD: sizespecializedmalloc added and defaulted on in the 1.26 cycle, off for the go1.26.0 release, on again in 1.27, flag removed in 1.28; specialization now covers size classes 1–7 (≤80 B); unconditional except on plan9/race/asan/msan/valgrind
Earlier history: Size classes, tiny allocator and mcache/mcentral (Go 1.1 → Go 1.17)
- tracking issue #79286 runtime: sizespecializedmalloc experiment referenced by 13 of this thread's commits
Design. In 1.24 mallocgc was split into independent paths by shape: mallocgcTiny, mallocgcSmallNoscan, mallocgcSmallScanNoHeader, mallocgcSmallScanHeader and mallocgcLarge. Each has its own specialized heapSetType* (8730fcf885, a1c4fb4361; MallocTypeInfo8 −31%). This was preceded by micro-cuts: MemProfileRate cached per mcache so a rate change forces the slow path (31437f25f2), debug checks behind a constant (6686edc0e7), the allocate-black check reading writeBarrier.enabled instead of gcphase (d8997c8c1f), and assistG recomputed instead of carried (8df6413e11). The 1.26–1.28 work goes further with a generator, runtime/_mkmalloc. It stamps templates from malloc_stubs.go into per-size-class functions in malloc_generated.go: mallocgcSmallNoScanSC*, mallocgcSmallScanNoHeaderSC*, mallocgcTinySC2, with dispatch tables in malloc_tables_generated.go (411c250d64). The generator does AST substitution and constant folding of booleans (21ebed0ac0, 2378242315). When the size is a compile-time constant, the compiler calls the specialized functions directly (19a30ea3f2). At HEAD mallocgc first indexes mallocNoScanTable/mallocScanTable by size. Each generated function has a fast path that runs only when debug.malloc, gcBlackenEnabled and secret mode are all off. It holds acquirem across the whole fast path so a GC cannot start mid-allocation (75560e67c9, 63b51fc270). Otherwise it falls to shared mallocgc*SlowPath functions, and post-allocation work is in postMallocgc (15fd4ff942). Instruction-cache footprint has been the limiting factor: per-size tiny functions were merged (−20% code, 9936a78b78), and coverage shrank from ≤512 B to ≤128 B (55089b9e27) and then to size class ≤7 (694604e524; generated code ~21k→16k).
Timeline.
- Go 1.24 — Done: micro-optimizations (
31437f25f2,6686edc0e7,d8997c8c1f,8df6413e11,e750a0cdb3,60ee99cf5d), path split (8730fcf885; −11.6% geomean), and specializedheapSetType(a1c4fb4361; −20.6% geomean). ASAN poisoning was fixed after the split (fb2a7f8ce1,4bf98186b5). Left open: “We can probably also do more with the size class lookups” (8730fcf885). The split dropped the fresh-span zeroing skip for large pointerful objects (fixed in 1.25,df9888ea4e1.25). - Go 1.25 — Done:
needzeroremoved from paths that must always zero (144d4e5d5f). Left open: nothing noted. - Go 1.26 — Done: user-visible
GOEXPERIMENT=sizespecializedmallocadded (60c1ee9183) and enabled by default (a7917eed70). Generator, templates and tables (a69395eab2,411c250d64); the size-class generator moved into_mkmalloc(f37d75472d); direct compiler calls (19a30ea3f2); valgrind support (c54dc1418b); generator polish (21ebed0ac0,2947cb0469,e2fef50def); an explicit nil check inheapSetTypeSmallHeaderto avoid read-then-write page faults (tile38 at GOMAXPROCS=192: +9.6% ops/s, p99 −15%,3032894e04). Left open: “I still need to investigate heapsampling.go” (411c250d64). - Go 1.27 — Done: turned off at cycle start for “regressions in overall icache footprint at scale” (
4af8ad24ee); the go1.26.0 tag shipped with it off via a release-branch cherry-pick. Then: tiny functions consolidated (9936a78b78); the fast path split (75560e67c9,15f44ffcc3); benchmarks (15129eb73b,24e654197a); classes >128 B and then >class 7 dropped (55089b9e27,694604e524); dead race/valgrind branches removed (0a151acad8,bfbbe9667e); refactors (722ee60825,15fd4ff942,c410b4944e,fe4da8e5c5); generator fixes (9301a3eab2,b23aea0c94,2378242315);acquiremacross the fast path (63b51fc270). Routing heap-bits-in-span through the slow path was tried and reverted as slower (dd1da37fa4,01d183b000). Re-enabled by default (2a93576965). Left open: “We may want to test moving the boundary even lower” (55089b9e27).freegcfor pointerful objects is disabled under specialization (TODO infreegcat HEAD). - Go 1.28 — Done: user-visible the GOEXPERIMENT was deleted, leaving the
sizeSpecializedMallocEnabledconstant (4a7645b730), plus ASAN dead-code cleanup (8a92448bc2). Left open: nothing noted.
Scavenger redesign
Span: Go 1.18 → Go 1.26 · Status at HEAD: controller-paced background scavenger at 1% of total CPU, chunk-index based search, memory-limit and GOGC goals
Paths not taken: Scavenger and heap-lock experiments before the page allocator rewrite (superseded)
Earlier history: The bitmap page allocator (Go 1.14 → Go 1.17) · Scavenger: returning memory to the OS (Go 1.0 → Go 1.17)
- proposal #48409 runtime/debug: soft memory limit referenced by 2 of this thread's commits
- design doc Proposal: Soft memory limit Michael Knyszek · linked from #48409
Design. The background scavenger returns free, unscavenged pages to the OS. It aims for retainExtraPercent = 10 above the GOGC heap goal and, under a memory limit, for 5% below the limit. Since 1.18 its rate is set by a PI controller over measured CPU time, targeting scavengePercent = 1 of total CPU instead of 1% of one CPU (e48e4b4cbb, 6f32d2050d). It works ≥1 ms per batch and does not hold the heap lock while releasing: pages being scavenged are marked allocated and then freed back (4f543b59c5). The 1.19 redesign (91f863013e) replaced the top-down address-space sweep, which “never retreads old ground”, with a lock-free scavengeIndex. The index holds one packed scavChunkData per 4 MiB page chunk (in-use pages, last-cycle in-use, generation, flags), and the page allocator marks chunks as it frees pages, so all scavengers see new work at once. The index costs ~2 MiB per TiB of contiguous heap. Since 1.21, a chunk that reached 96.875% occupancy (scavChunkHiOccFrac) during a GC cycle is hidden from the background scavenger for that cycle and the next. Memory-limit and FreeOSMemory scavenging ignore this.
Timeline.
- Go 1.18 — Done: PI controller (
e48e4b4cbb), atomicscavengeGoal(fc5e8cd6c9), lock-free scavenging (4f543b59c5,6d1fffac63), the 1% total-CPU target (6f32d2050d), a released-bytes accumulation fix (f986191325), and a conservative fallback when the controller overflows (e4a173adf6). Left open: the scavenger “is not really in a testable state” (e4a173adf6). - Go 1.19 — Done: scavenger refactored into a testable
scavengerState(d29f5247b8); chunk-index redesign (91f863013e,ccb798741b); memory-limit goal and allocation-time scavenge assist (b4d81147d8); physical-page-aligned allocations restructured so scavenged-bit accounting stays exact (b58067013e). Left open: finding work can mean iterating “across the whole heap”, giving “worse overheads for memory limit scavenging” (91f863013e). - Go 1.20 — Done:
searchIdxused inscavengeOneto avoid N² behavior within a chunk (2caaad942e), and padding removed (f324355d1f). Left open: nothing noted. - Go 1.21 — Done: idle Ps’ page caches flushed at the end of each GC so the scavenger can see them (
8d68b388d4); index initialization andminHeapIdxrestored (8588a3fa08,15c1276246); eager-scavenge statistics inscavtrace(a3e90dc377); density tracking (huge-page thread). Left open: nothing noted. - Go 1.22 — Done: corrected memory-limit goal (
729f214e3a);sysGrowmin-index bug that remapped and zeroed live index data (c95fe91d07). Left open: nothing noted. - Go 1.26 — Done: on Linux, memory released with
MADV_DONTNEEDis known to be zero, so software zeroing is skipped on reuse (27937289dc); a duplicatescavengeIndex.sysGrowcall removed (4fca79833f). Left open: nothing noted.
Transparent huge page management
Span: Go 1.21 → Go 1.22 · Status at HEAD: explicit heap THP marking removed in 1.22; density-based scavenger deferral and metadata huge-page toggling remain; GODEBUG=disablethp available
- design doc A more hugepage-aware Go heap Michael Knyszek, Michael Pratt · golang/proposal
- tracking issue #59960 runtime: improve heap hugepage utilization referenced by 2 of this thread's commits
Design. In 1.21 the runtime took over THP policy on Linux (8fa9e3beee). New memory was marked MADV_HUGEPAGE, chunks that became dense within a GC cycle stayed huge and hidden from the scavenger, and before scavenging a sparse chunk the runtime marked it MADV_NOHUGEPAGE so khugepaged would not re-coalesce it. GC metadata (the arena index and page-allocator chunks) is MADV_NOHUGEPAGE for small heaps and flipped to huge pages once the heap grows (1f9d80e331, enableMetadataHugePages). The heap part failed in production. MADV_HUGEPAGE triggers unbounded synchronous direct reclaim and compaction (stalls of “100s of ms or even seconds”), so 1.22 switched to MADV_COLLAPSE (Linux 6.1+) and stopped using MADV_NOHUGEPAGE (9f9bb26880). That was then disabled as well (595deec3dd), and the tracking code deleted (765dfb00b2).
Timeline.
- Go 1.21 — Done: explicit policy with 96% density and 1-GC-cycle hysteresis (
8fa9e3beee),sysNoHugePage(84eaceaba7) with a page-alignment check (ada0eec827), and metadata huge pages only for large heaps (1f9d80e331). Left open: “The main risk here is churn” (8fa9e3beee). - Go 1.22 — Done: user-visible
MADV_COLLAPSEinstead ofMADV_HUGEPAGE(9f9bb26880), then “don’t eagerly collapse hugepages … Disable it until further notice” (595deec3dd), with dead code deleted (765dfb00b2). Metadata huge-page calls moved to the system stack to avoid a self-deadlock on stack growth (5f08b44799).GODEBUG=disablethpwas added, because 1.21.1+ stopped working around the kernel’s khugepaged behavior (c915215af6). Left open: the runtime no longer stops khugepaged from re-backing returned memory: “basically unavoidable without a new madvise flag” (9f9bb26880).
Finalizers, runtime.AddCleanup and the cleanup queue
Span: Go 1.19 → Go 1.28 · Status at HEAD: AddCleanup shipped in 1.24; cleanups run on up to GOMAXPROCS goroutines since 1.25; GODEBUG=checkfinalizer leak detector since 1.25; misuse panics since 1.26
Earlier history: Finalizers and span specials (Go 1.0 → Go 1.17)
- proposal #67535 runtime: add AddCleanup referenced by 3 of this thread's commits
- proposal #71825 runtime: allow cleanups to run concurrently referenced by 2 of this thread's commits
Design. runtime.AddCleanup(ptr, fn, arg) attaches a specialCleanup to the object’s span, like a finalizer special but without resurrecting the object. Since 1.26 arg is stored in its own allocation; tiny arguments are padded to 16 bytes so they cannot share a tiny block with ptr and keep it alive (1bb1f2bf0c). Cleanup.Stop removes the special (6a2fb15475). In 1.25 cleanups got their own queue (e46c8e0558). Sweepers enqueue into P-local fixed-size off-heap blocks, which are GC roots like finalizer blocks. Full blocks move to a full list, and the last sweeper flushes all local blocks without synchronization. Only whole blocks are dequeued, by a pool of cleanup goroutines. The scheduler wakes them, and when demand exceeds the pool it signals AddCleanup to create another goroutine, up to GOMAXPROCS, because neither the sweeper nor the scheduler can create goroutines. Cleanups may therefore run concurrently (1421b982dc). GODEBUG=checkfinalizer=1 runs a partial checkmark GC from specials only and reports any path from a finalizer/cleanup back to its own object (3df078fc74). Reports are annotated with source and type via a CheckFinalizer special (913c069819), and checkfinalizer>1 adds a scan trace (eec8dd0836).
Timeline.
- Go 1.19 — Done: the finalizer
reflectcallframe did not include the register-ABI spill slot and could clobber the caller; fixed (58804ea67a). Left open: nothing noted. - Go 1.20 — Done: user-visible documentation that finalizers on tiny (<16 B) objects may never run (
80d487111b); atomicfingStatus(−5% Finalizer benchmark,bd5595d7fa); test cleanup (eaf0e3d465). Left open: nothing noted. - Go 1.21 — Done: test portability only (
fbf14ae166). Left open: nothing noted. - Go 1.23 — Done: user-visible doc note on method receivers in
SetFinalizer(f7c330eac7). Left open: nothing noted. - Go 1.24 — Done: user-visible
AddCleanup(0531768b30) andStop(6a2fb15475). Fixes: thestopsearch could loop or corrupt non-cleanup specials (7f049eac1b); the closure was kept alive while the special became visible (d69e6f63c3); thearg == ptrcheck (9d21ef3bd4).SetFinalizervalidates arguments before the GC-mode check (252e9def65). Docs (8c3e391573) and race-detector tips in zombie reports (09f9b5e361). Left open:SetFinalizeris to be deprecated (0531768b30). - Go 1.25 — Done: user-visible
checkfinalizer(3df078fc74,913c069819,eec8dd0836,c58f58b9f8,8b45a3f78b,5b0b4c01ba), with queue lengths printed each cycle (0d42cebacd); finalizer blocks cleaned up (57cd75a351); the multi-goroutine cleanup queue (e46c8e0558,1421b982dc); a missed-wakeup fix by re-checking the full list after marking asleep (b450b5409d); nil-P skip inprocresize(8dd7d2111b); race-detector annotations (3ea94ae446).sync,reflectanduniquemoved fromSetFinalizertoAddCleanup(954e2c0b06,b4e992b6e1,0028532118). Left open:c58f58b9f8flagged that a tiny-allocated cleanup arg “could end up co-located with the object”; fixed in 1.26 (1bb1f2bf0c1.26). - Go 1.26 — Done: user-visible panics when
ptr == argor when the cleanup closure capturesptr(06412288cf,eb63ef9d66); separate argument storage (−16.7% ns, −33% B/op,1bb1f2bf0c); benchmark (9fd2e44439);/gc/cleanups/…and/gc/finalizers/…queue metrics (a4d99770c0); test log (43ebed88cc). Left open: nothing noted. - Go 1.28 — Done: cleanup argument layout uses the static
abi.TypeFor[S], fixing interface arguments hidden as noscan on 32-bit (c456de74b8); a faster specials-list walk (Finalizer −1.2%,1c72b3d2c1). Left open: nothing noted.
Weak pointers and the unique package
Span: Go 1.23 → Go 1.28 · Status at HEAD: unique since 1.23, public weak since 1.24; unique uses a bespoke weak canonicalization map since 1.25
- proposal #67552 weak: new package providing weak pointers referenced by 1 of this thread's commits
- proposal #62483 unique: new package with unique.Handle referenced by 2 of this thread's commits
- motivating issue #43615 proposal: container: add weak reference maps listed in Go: A Documentary
Design. A weak pointer is a pointer to an 8-byte off-heap handle (atomic.Uintptr), created by getOrAddWeakHandle, which attaches a specialWeakHandle to the object. All weak pointers to one object (at one offset; offsets are uintptr since d87878c62b) share the handle, so weak.Pointer equality is object identity (5ec76ae5aa). When the sweeper frees the object it stores 0 into the handle. Value() (makeStrongFromWeak) loads the handle, calls ensureSwept on its span, re-reads, and then shades the pointer like a Yuasa barrier, because the newly strong pointer may land in an already-black stack (79fd633632). Because shading during mark termination would create new non-monotonic GC work, conversions park on work.strongFromWeak.block around the ragged barrier (80d306da50). Linker-allocated and sbrk-mode objects use an “immortal” lock-free handle map instead of specials (6d7cb594b3, 53b9eae387). unique.Make first used a HashTrieMap[T]→weak.Pointer with a background cleanup goroutine (a088e230d4). In 1.25 it became a bespoke concurrent canonicalization map holding only weak keys, cleaned up through AddCleanup, so a chain of N nested handles is freed in one GC instead of N (0028532118).
Timeline.
- Go 1.23 — Done: user-visible
internal/weak(dfc86e922c) and theuniquepackage withMake/Handle(a088e230d4); string clone viastringslite(4dccb78582);HashTrieMapiteration (3a3837f422). Left open: the weak API was internal only. - Go 1.24 — Done: user-visible the public
weakpackage withStrongrenamed toValue(a65f1a467f) and docs (c46ba1f9ef,adfd81d8cd,07398d2e57,87dbfb9fa7,772f024c61). Correctness: shading on conversion (79fd633632), blocking during mark termination (80d306da50, test3bd08b9792), keeping the handle alive while the special is published (88cfad0c89),uintptrspecial offsets for interior weak pointers >64 KiB (d87878c62b), and a zeroPointer.Valuereturning nil (d62154db83). unique: stored keys are the clone, not the input (21ac23a96f); zero-size types (2927aa10ab);TypeFor(755c18ecdf); tests and docs (23c9efa244,1596a6c8ec,b7e4d2067c,a785d11ac4).HashTrieMapmoved tointernal/syncwith a usable zero value (4804d0daca,e8c5e6d635). Left open: the special offset could later be packed with the kind to recover memory (d87878c62b). - Go 1.25 — Done: user-visible
weak.Makeaccepts linker-allocated objects (6d7cb594b3);Pointerconversions between types are blocked (8163ea1458); equality semantics clarified (5ec76ae5aa); weak pointers work in sbrk mode (53b9eae387). The unique canon map withAddCleanuplanded (0028532118,e382bf5b32,2f036e1475,ca3b474702,644905891f). Compiler help forunique.Make: no copy forstring(b)(27ff0f249c) and modeled data flow of non-string pointers for escape analysis (5e6a868b28). Left open: “We still need some kind of back-pressure mechanism” for cleanups (0028532118). - Go 1.26 — Done: weak handles padded to 16 bytes so they bypass the tiny allocator and cannot leak neighbors (
80c91eedbb); canon map fixes (ffa882059c,47bf8f073e); test (a33bbf1988). Left open: “We could potentially do better by using the 8 byte noscan size class” (80c91eedbb). - Go 1.28 — Done:
unique.Makeexample (232c9e1160). Left open: nothing noted.
Object pinning (runtime.Pinner)
Span: Go 1.21 → Go 1.28 · Status at HEAD: runtime.Pinner shipped in 1.21; 1.28 cut pin/unpin costs with batching and per-P caches
- proposal #46787 runtime: provide Pinner API for object pinning referenced by 3 of this thread's commits
- motivating issue #40431 proposal: runtime.Pin() to protect against hypothetical moving GC in the future linked from #46787
Design. Pinner.Pin lets Go memory be referenced from C memory (iovec, io_uring) without copying. The original version (251daf46fb) used a fixed per-span bitmap. In 1.21 pin state moved into GC-bitmap-arena gcBits with two bits per object: pinned, and multi-pinned, meaning a specialPinCounter exists. These bits are copied each GC cycle (697644070c). Cgo pointer checks read the bits without a lock. The *pinner holds a preallocated 5-entry refs array (4bb38fe458) and is cached on the P, which took PinUnpin from 592 ns to 40 ns with zero allocations (0adcc5ace8). A Pinner that becomes unreachable while still holding pins panics. In 1.28 pin-bit updates under the span’s specials lock became plain atomic stores, redundant updates are skipped, unpinning is batched per span (≤64 refs, PinUnpinBatch −38%), and one pin counter is cached per P (91fa326dfe, a69abce146, d6eb7bd6ae, 3544f81e5d).
Timeline.
- Go 1.21 — Done: user-visible the
PinnerAPI (251daf46fb), gcBits-based state (697644070c), ref preallocation and P cache (4bb38fe458,0adcc5ace8, benchmarks94c75232fb,5d68121628), lock-rank edges (78df00dd6f,807cce479f), and docs (c7b2f6494c). Alongside:heapObjectsCanMove()(always false) forgo4.org/unsafe/assume-no-moving-gc, so that package stops breaking on each release (055c186f53,789701e93a). Left open: nothing noted. - Go 1.22 — Done: user-visible
Pinsilently accepts non-Go pointers (bfa8a8586f), with reworded docs (451e4727ec). Left open: nothing noted. - Go 1.23 — Done:
runtime.debugPinnerV1, a self-pinning Pinner for debuggers usingdebugCall(7f9edb4225). Left open: nothing noted. - Go 1.26 — Done: documentation (
856238615d,76345533f7). Left open: nothing noted. - Go 1.28 — Done: atomic stores instead of RMW (
91fa326dfe), skipping redundant bit updates (a69abce146), setting the specials bitmap only for the first special and skipping sweeper registration for swept spans (PinUnpinWithGC −32.5%,7d93750c51), per-span unpin batching (d6eb7bd6ae), a shorter specials lookup (1c72b3d2c1), per-P pin-counter cache (3544f81e5d), and no system-stack switches, since pin bits no longer depend on the heap lock (3017a6ec16). Left open: nothing noted.
Memory arenas experiment
Span: Go 1.20 → Go 1.26 · Status at HEAD: GOEXPERIMENT=arenas (package arena, reflect.ArenaNew) present and off by default since 1.20; no further feature work
Paths not taken: Arena API prototypes, 2017–2022 (partially landed)
- proposal #51317 proposal: arena: new package providing memory arenas referenced by 5 of this thread's commits
- design doc Memory regions golang/proposal
- discussion memory regions
Design. User arenas (arena.go) allocate from chunks of min(8 MiB, heap arena size). Only objects ≤¼ chunk are accepted, bounding fragmentation at 25%. Free does not unmap a chunk. It makes the chunk fault, puts it on an evacuation list, and reuses it only after a GC finds no remaining pointers into it. ASAN, MSAN and race are supported (7866538d25). Arena chunks get heap bits written per object (userArenaHeapBitsSetType) and are deliberately not registered as ordinary heap arenas. The runtime work was preceded by refactors of span initialization, assist credit and sysAlloc hint lists (69fc74f3ee, 987f94fa03, 4c383951b9).
Timeline.
- Go 1.20 — Done: user-visible runtime arenas (
7866538d25) and thearenapackage withreflect.ArenaNewbehind a GOEXPERIMENT (e0d01b8467); refactors (69fc74f3ee,987f94fa03,4c383951b9); a test skip (e72da1c15d). Left open: “A later CL can potentially export it” (7866538d25); it stayed experimental. - Go 1.21 — Done: user-visible
reflect.ArenaNewchanged to takeTand return*T, a breaking change (2b0ff4b629). Left open: nothing noted. - Go 1.22 — Done: adapted to allocation headers (
cdf1362179); big-endian heap-bit byte order (f7c5cbb820); clearingmspan.largeTypewithout a write barrier (052d402985); test (90a73ec1f2). Left open: “Much of this can be avoided … if the compiler were to write out the pointer/scalar bits as an array of uintptr” (f7c5cbb820). - Go 1.26 — Done: assist credit for arena chunks deducted only during GC, restoring behavior lost in the 1.24 malloc split (
d79405a344); test skips (1a03d0db3f,96e142ba2b). Left open: nothing noted.
runtime.freegc: compiler-directed explicit freeing
Span: Go 1.26 → Go 1.26 · Status at HEAD: GOEXPERIMENT=runtimefreegc present, off by default; noscan objects only
- tracking issue #74299 runtime, cmd/compile: add runtime.freegc and runtime.freegcTracked to reduce GC work (GOEXPERIMENT=runtimefreegc) referenced by 5 of this thread's commits
- design doc Directly freeing user memory to reduce GC work cited in this thread's commits
- discussion a safe free of memory proposal, runtime.FreeMemory() golang-nuts · linked from #74299
Design. Design 74299-runtime-freegc: freegc(ptr, size, noscan) is a linkname-blocked runtime entry point meant only for the compiler. It pushes a dead object onto mcache.reusableNoscan[spanClass], a per-P linked list. The next allocation in that size class pops it before nextFreeFast, so memory is reused without waiting for a GC cycle. Assist credit is adjusted so reuse is neutral (120f1874ef). GODEBUG=clobberfree=1 fills freed memory with 0xdeadbeef, which the GC treats as fatal. Freeing is a no-op for stack pointers, sizes <16 B or >32 KiB−header, and when race/asan/msan/valgrind are on. A modified strings.Builder ran ~2× faster, and an experimental reflect patch cut json/v2 allocation by up to 43.7% (fecfcaa4f6). The only compiler user so far is alias analysis in escape analysis. When a slice is provably unaliased across append, the compiler calls growsliceNoAlias, which frees the old backing array (4879151d1d).
Timeline.
- Go 1.26 — Done: user-visible
freegcwith reuse lists and clobberfree (fecfcaa4f6), an assist-credit test (120f1874ef), size-specialized noscan reuse (50128a2154), the experiment renamed toruntimefreegcand off for 1.26 (5a347b775e), and compiler alias analysis withgrowsliceNoAlias(4879151d1d, compiler area). Left open:freegc“throws if noscan is false (support for which is implemented in a later CL)”. The specialized-malloc path disables pointerful freeing via a TODO, andclobberfree=2(mprotect) is described as a later experimental CL (fecfcaa4f6,50128a2154). None of these has landed at HEAD.
Mark worker scheduling and GC work queues
Span: Go 1.19 → Go 1.26 · Status at HEAD: idle workers capped and suppressed when dedicated workers exist (1.19); worker selection integrated into start-the-world (1.26)
Earlier history: Concurrent mark: write barriers and the dev.garbage collector (Go 1.4 → Go 1.6) · Decentralized GC state machine and mark termination simplification (Go 1.6 → Go 1.12) · Mark work distribution: parfor, work buffers, gcWork and root jobs (Go 1.1 → Go 1.17)
- motivating issue #44163 runtime: remove idle GC workers for the 2-minute GC cycle referenced by 2 of this thread's commits
- motivating issue #37116 runtime: 10ms-26ms latency from GC in go1.14rc1, possibly due to 'GC (idle)' work referenced by 1 of this thread's commits
Design. Dedicated, fractional and idle mark workers are scheduled from findRunnable. In 1.19, idle workers in periodic (2-minute) GCs were capped at 1. They had produced 100%-of-GOMAXPROCS CPU spikes in idle services that confused autoscalers (e1b5f347e7). Once stopped Ms could reliably become dedicated workers (#44313), idle workers are not needed when a dedicated one runs (d8cf2243e0). At large GOMAXPROCS, contention on the shared gcBgMarkWorkerPool CAS was avoided by checking first whether a worker is needed at all (e50913cefc). In 1.26 findRunnableGCWorker was split. The selection half, assignWaitingGCWorker, is safe during STW, so procresize now assigns mark workers to previously idle Ps before restarting the world instead of chaining wakeps (829779f4fe, a18aff8057).
Timeline.
- Go 1.19 — Done: periodic-GC idle-worker cap (
e1b5f347e7); idle workers disabled when a dedicated worker exists (d8cf2243e0); merged wbuf checks intryGetFast/putFast(86c8075675). Left open: idle workers are still “necessary for GC progress” when only fractional workers exist (e1b5f347e7). - Go 1.21 — Done:
work.full/work.emptypadded against false sharing (gRPC +8%, DeathStarBench +9.7%,bdc5533f39); duplicatework.fullcheck removed (9411075748). Left open: nothing noted. - Go 1.22 — Done: all mark workers yield to
forEachP(needed by the new tracer,25895d1c99); per-worker-kindgcDrainwrappers so profiles show which kind is running (081d27a2e5). Left open: nothing noted. - Go 1.23 — Done:
gcBgMarkStartWorkersuses channels instead of notes, sorunnextcan hand off directly (5dceddfe7c); data/bss root jobs computed in one loop (0b1b6c1589). Left open: nothing noted. - Go 1.24 — Done: futile worker acquisition avoided (
e50913cefc). Left open: nothing noted. - Go 1.26 — Done: worker selection during start-the-world (
829779f4fe,a18aff8057); atomicp.gcFractionalMarkTime(d3aeba1670). Left open: if fewer than 25% of Ps are idle, some user Ps still turn into dedicated workers, and stack-scan resumes still migrate goroutines (a18aff8057).
Allocation publication, zeroing and preemptibility
Span: Go 1.18 → Go 1.27 · Status at HEAD: allocate-black plus freeIndexForScan snapshot; largeType as atomic publication point; preemptible large clears still pending
Earlier history: Allocating from mark bits: allocBits, freeindex and ctz (Go 1.7 → Go 1.10)
- motivating issue #31222 runtime: long pauses STW (sweep termination) on massive block allocation referenced by 1 of this thread's commits
- motivating issue #69327 runtime: non-preemptible zeroing in clear, append, bytes.growSlice, etc referenced by 1 of this thread's commits
Design. The conservative scanner (async-preempted frames) can find “out of thin air” pointers to objects that are being allocated right now. In 1.20 the scanner switched to a separate freeIndexForScan, advanced only after initialization and a publication barrier (febe7b8e2a). In 1.25 this was simplified: freeIndexForScan is updated only outside the mark phase, so it is a snapshot from mark start, and objects allocated during marking are black anyway. Conservative scanning uses isFreeOrNewlyAllocated (24ea1aa25c). Large objects publish via span.largeType, which became atomic so that the GC sees initialized memory only once it is non-nil (df9888ea4e). Large pointerful objects are zeroed preemptibly by temporarily giving their span no type, possible only with allocation headers (de3a3c9ebc).
Timeline.
- Go 1.18 — Done: zeroing unified in
mallocgc. Small-object spans from mheap are never zeroed eagerly, and the flag becamedelayZeroing, used only for pointer-free large objects (d8fc7f785e). Left open: nothing noted. - Go 1.20 — Done: the scanner reads
freeIndexForScan(febe7b8e2a); an accidental submit of the alternative design was reverted (bed2b7cf41,2b59307ac2); Malloc benchmarks fixed (7717ac151a). Left open: nothing noted. - Go 1.23 — Done: preemptible zeroing of large pointerful objects (
de3a3c9ebc). Left open: nothing noted. - Go 1.24 — Done: a publication barrier for delayed-zeroed noscan large objects, for user code on weak memory (
acd072a078). Left open: nothing noted. - Go 1.25 — Done: the fresh-from-OS zeroing skip restored plus atomic
largeType(df9888ea4e); thefreeIndexForScansnapshot (24ea1aa25c). Left open: nothing noted. - Go 1.27 — Done: a preemptible
memclrNoHeapPointersPreemptiblefor large pointer-freeclear()and allocations, chunked at 256 KB, landed (50126a8e44). A stack-pointer fix followed (cd2fa3b48d). Both were reverted because preemptible clears “expose existing unsafe use of notInHeapSlice” (16018b05ae,5f5f4ccdb3). Left open: #69327 (non-preemptible multi-MB clears blocking STW) remains open pending thenotInHeapSlicefix.
Sweeper termination and pacing
Span: Go 1.18 → Go 1.28 · Status at HEAD: single-atomic sweep termination; bgsweep yields only when no Ps are idle
Paths not taken: Asynchronous sweeping for small allocations (never landed)
Earlier history: Concurrent sweep and proportional sweep (Go 1.3 → Go 1.17) · Concurrent mark: write barriers and the dev.garbage collector (Go 1.4 → Go 1.6)
- motivating issue #45315 runtime: runtime.GC can return without finishing sweep referenced by 1 of this thread's commits
Design. In 1.18 mheap_.sweepers and sweepDrained were merged into one atomic (activeSweep). A sweeper registers only while unswept spans remain, so isSweepDone cannot flap (3aecb3a8f7). bgsweep used to Gosched after every span, emitting trace events every ~30 ns. Since 1.20 it sweeps ≥10 spans and yields only if no P is idle (8451529e9a).
Timeline.
- Go 1.18 — Done: atomic sweep termination (
3aecb3a8f7);GCSweepStarttrace event fix (903f313d8b). Left open: nothing noted. - Go 1.20 — Done: bgsweep heuristic (
8451529e9a, “may be worth considering pushing this number higher”); unused sweepdata field removed (2df6c1abce). Left open: nothing noted. - Go 1.21 — Done: overflow in the sweep-assist delta could pin all allocators in sweeping until termination; fixed (
a7b75972f2). Left open: nothing noted. - Go 1.22 — Done:
GODEBUG=gcstoptheworld=2now flushes mcaches ingcSweep,_ConcurrentSweepremoved (4f4c23512e); write-only fields deleted (7241fee9b0). Left open: nothing noted. - Go 1.23 — Done: large-object stats updated before
freeSpan, fixing/memory/classes/heap/unusedunderflow (36d32f68f4). Left open: nothing noted. - Go 1.25 — Done:
goschedIfBusyin bgsweep to avoid a livelock oncelockis inlined (e666f1dabf); gcpacertrace end-of-sweep condition (ad7a6f8159). Left open: nothing noted. - Go 1.28 — Done: replacing
finishsweep_m’s sweep loop with a throw (5df35d3ce5) was reverted, because concurrentgcStarts can leave unswept spans (9684147f09). The reasons are now documented (8e0e1f7e99). Left open: #81957 remains open.
Stack and register root scanning
Span: Go 1.18 → Go 1.28 · Status at HEAD: allgs snapshot at mark start; async-preemption register state (scalar, vector, LR frame) scanned conservatively
Earlier history: Stack scanning: conservative, precise by frames, stack barriers, hybrid barrier, async safe points (Go 1.1 → Go 1.14) · Hybrid write barrier and buffered write barrier (Go 1.8 → Go 1.12)
Design. Roots are globals (data/bss bitmaps), goroutine stacks (precise via stack maps, conservative for async-preempted frames), finalizer/cleanup blocks and specials. The work here keeps that set complete as the runtime changed: racing allgs growth, new suspendG users (execution tracer, mutex profiling) that can deadlock with STW or race with stack shrinking, and new places where pointers live only in registers at an async preemption point (vector registers used for small moves, the arm64 LR spill frame).
Timeline.
- Go 1.18 — Done:
markrootreadallgsconcurrently withallgaddand could see nil on weak memory;gcMarkRootPreparenow snapshots the slice header during STW (08ecdf7c2e). Alsoclobberdeadpointer crash support on ARM64 (55b4310acd). Left open: nothing noted. - Go 1.25 — Done: GC STW now calls
casGToWaitingForSuspendG, avoiding mutual deadlock with the tracer’ssuspendG(c6ac736288). Left open: nothing noted. - Go 1.26 — Done: stack shrinking disabled whenever a goroutine waits for
suspendG(tracer, mutex profiling), instead of trying to be shrink-safe (bd07fafb0a). Left open: nothing noted. - Go 1.27 — Done: extended (vector) register state of async-preempted goroutines is scanned conservatively, so vectorized moves may carry pointers (
683aa8893a). On arm64 the 16-byte frame holding R30 frompushCallis scanned (a129840779). Left open: nothing noted. - Go 1.28 — Done: register save blocks are no longer zeroed, since
xRegSaveoverwrites them before publication (8f71bead49);clobberdeadon loong64 (2c3e97ac68). Left open: nothing noted.
Address-space layout, tagged pointers and the mem.go layer
Span: Go 1.18 → Go 1.28 · Status at HEAD: per-platform heapAddrBits with 9 low tag bits; all OS mapping goes through mem.go state transitions
Earlier history: The bitmap page allocator (Go 1.14 → Go 1.17) · Heap address space: contiguous arena to sparse arenas (Go 1.0 → Go 1.17)
Design. mem.go defines the OS abstraction as state transitions (None→Reserved→Prepared→Ready) with mandatory sysMemStat accounting (f990b0f1e8, 4649a43903). In 1.27 sysUnreserve was added so LSAN root regions are always unregistered, and no sysFreeOS calls remain outside mem.go (40ec033c33). taggedPointer (generalized from lfstack, d959dd97b1) packs a tag into the unused high and low bits. The low bits went from 3 to 9 by requiring 512-byte alignment of taggable objects (9d0320de25).
Timeline.
- Go 1.18 — Done: ios/arm64 moved to 40 address bits with the 64-bit incremental page allocator (
c27a3592ae); mmap failure printed (bb7fb8a5fa). Left open: the exception for iOS <14 stays “Once iOS <14 is deprecated” (c27a3592ae). - Go 1.19 — Done:
sys*wrappers and consolidated docs (f990b0f1e8). Left open: nothing noted. - Go 1.20 — Done:
arenaIdxaccessors made nosplit (506e690a26). Left open: nothing noted. - Go 1.21 — Done:
lfstack→taggedPointer(d959dd97b1); fallback tommapwhenmadviseis compiled out of Linux (5ffdc1f15c). Left open: nothing noted. - Go 1.24 — Done: iOS address-bit docs (
cdca671855). Left open: nothing noted. - Go 1.25 — Done: user-visible anonymous mappings named via
PR_SET_VMA_ANON_NAME(e.g.[anon: Go: page summary]), Linux 5.17+ (52eaed6633). 512-byte tag alignment (9d0320de25, aix fixes3009566a46,0c15d88974); freebsd/amd64 to 57 bits for LA57 (5bbac667c5). Left open: nothing noted. - Go 1.26 — Done: one more address bit for tagged pointers for amd64-on-arm64 emulation (
2a7f1d47b0); riscv64 sv39/48/57 detection, cutting startup from ~85 mmaps/66 munmaps to 22/0 on sv39 (dae9e456ae); gccgo build tags (643f80a11f). Left open: nothing noted. - Go 1.27 — Done:
sysUnreserveand LSAN coverage (40ec033c33),sysFreedocs (4ce2612f21). Left open: “I am not completely confident that we always pass the full region to sysFree” (40ec033c33). - Go 1.28 — Done: openbsd/arm64 corrected to 39 address bits (
3a5e97de8b). Left open: nothing noted.
Linear-memory heaps (Wasm and sbrk platforms)
Span: Go 1.21 → Go 1.27 · Status at HEAD: Wasm and Plan 9 share the sbrk free-block allocator; Wasm uses 512 KiB arenas
Design. On platforms with a single growable linear memory (Wasm, Plan 9) mem_sbrk.go emulates reservations by bumping a break under memlock and keeps a free-block list so sysFreed regions are reused, since memory cannot be returned to the OS. The work reduced waste from aligned reservations and large arenas, which matter for small WASI plugins, and fixed locking.
Timeline.
- Go 1.21 — Done: Wasm reuses blocks freed via
sysFreeOS, sharing Plan 9’s sbrk strategy (dac514ea37). Left open: nothing noted. - Go 1.24 — Done: Wasm uses unused linear memory at the break, including linker-reserved initial memory (
4510586f93); aligned reservations round the break instead of over-reserving, and sbrk systems drop arena hints (22664f33b7). Left open: nothing noted. - Go 1.25 — Done: missing unlock in
sysReserveAlignedSbrk(456a90aa16). Left open: nothing noted. - Go 1.26 — Done: user-visible Wasm heap arena and page-chunk size reduced to 512 KiB; hello-world drops from 8 MB to <3 MB linear memory (
53009b26dd). sbrk mem.go switches to the system stack to avoidmemlockself-deadlock (11d5484190). Left open: nothing noted. - Go 1.27 — Done: nil pointer handling in
sysFreeOSon sbrk (649897800b). Left open: nothing noted.
Heap base address randomization
Span: Go 1.26 → Go 1.28 · Status at HEAD: GOEXPERIMENT=randomizedheapbase64 on by default since 1.26 (off under race/asan/msan)
- motivating issue #27583 runtime: feature request: Support for ASLR with the heap referenced by 1 of this thread's commits
- discussion Secure Go binaries golang-nuts · linked from #27583
Design. At init a random heapArenaBytes-aligned address seeds the arena hints. The first mheap.grow picks a random PallocChunkBytes offset, and a random number of pages is marked allocated. The resulting entropy is at least heapAddrBits − PageShift − 1 bits (6669aa3b14).
Timeline.
- Go 1.26 — Done: user-visible randomization (
6669aa3b14), on by default (72ba117bda), disabled with ASAN/MSAN (77aac7bb75), test fix (465b85eb76). Left open: nothing noted. - Go 1.28 — Done: the prefix mask was misaligned by up to 2 bits, so in ~75% of launches hints collapsed in pairs or quads; fixed (
fb8616482c). Left open: nothing noted.
Changes in this slice that are not part of a thread are listed on the release pages: 1.18 · 1.19 · 1.20 · 1.21 · 1.22 · 1.23 · 1.25 · 1.26 · 1.27