Write Barriers and Mark Assist

Updated

September 8, 2026

Write Barriers and Mark Assist

Overview

During concurrent mark, mutators keep running. Write barriers intercept pointer stores so the GC does not lose objects. When allocation outruns marking, mark assist makes allocating Gs help mark—coupling alloc rate to GC CPU on the request path.

Diagram: barrier keeps tri-color safe

  mutator: x.f = p
       │
       v
  write barrier (during concurrent mark)
       │
       v
  shade / publish pointer to GC

  alloc while GC behind ──► mark assist on same G

Without barriers, a black object could gain a white pointer the GC will never see → use-after-free class bugs.

When barriers are on

  • Enabled around concurrent mark
  • Cost shows up as extra work on pointer-heavy writes
  • Reducing pointer density (flatten structs, fewer interfaces in hot graphs) helps

Mark assist

mutator allocates fast
  → GC behind on mark
  → allocator charges assist credit
  → G marks a bit before continuing
sequence (top → bottom):
  actors: mutator G, allocator, mark work
  mutator G --> allocator  : mallocgc
  allocator --> mutator G  : assist debt
  mutator G --> mark work  : scan some greys
  mutator G --> mutator G  : continue user code

Symptom: latency spikes on alloc-heavy handlers correlating with GC cycles—not only STW.

Tuning angle

  1. Allocate less (best)
  2. GOMEMLIMIT / GOGC shape frequency
  3. Profile runtime.gcAssistAlloc / mark symbols
  4. Avoid panicking into “just raise GOGC” without heap proof

Experiment

GODEBUG=gctrace=1 go run .
package main
import ("fmt"; "runtime")
func main() {
    var hold [][]byte
    for i := 0; i < 2000; i++ {
        hold = append(hold, make([]byte, 64*1024))
    }
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Println("NumGC", m.NumGC, "GCCPUFraction", m.GCCPUFraction)
    _ = hold
}

What to notice: gctrace lines and rising NumGC under allocation bursts.

Try next: Compare assist-heavy run vs pre-sized slice with fewer growths.