Week 4 - Escape Analysis and the Inliner¶
4.1 Conceptual Core¶
- Escape analysis is the compiler pass that decides whether a variable can live on the stack (cheap, freed on return) or must live on the heap (allocated by
mallocgc, GC-tracked). It is not a runtime decision; it is purely static. - The two questions the compiler asks for each
&x/new(T)/make(...): - Does the address outlive the current function? (Returned, stored in a heap object, captured by a goroutine, captured by an interface escape.)
- Is the size statically known and bounded? (Variable-length stack allocations are limited.)
- If yes to escape: heap. If no: stack. The compiler dumps its reasoning under - gcflags=-m`.
4.2 Mechanical Detail¶
- Common escape triggers:
- Returning
&xfor a localx. - Storing
&xin a heap-allocated struct, slice, or map. - Capturing
xby a goroutine closure (the goroutine outlives the frame). - Boxing a value into an
interface{}of nontrivial size-the value is copied to the heap so the interface header can hold a pointer. - Calls to functions whose parameters are passed via
interface{}(e.g.,fmt.Printf("%d", x)boxesx). - Slices grown beyond the inlined-make size threshold.
- The inliner: small functions are inlined. Inlining matters for escape analysis because escape decisions are made across inlined call sites-a function that "would escape if not inlined" may stay on the stack when inlined into its caller.
//go:noinlineand//go:nosplit: directives to suppress inlining or stack-split checks. Reserved for runtime-internal code; rarely justified in application code.- Allocation profile:
go test -bench=. -memprofile=mem.outthengo tool pprof -alloc_objects mem.out. The - alloc_objectsview counts allocations (escapes); - inuse_spacecounts retained bytes.
4.3 Lab-"Escape Forensics"¶
For each of the following snippets, predict whether the value escapes, then verify with - gcflags=-m:
1.func A() int { x := 7; return &x }2.func B() int { x := 7; p := &x; return p }3.func C() { x := 7; go func() { fmt.Println(x) }() }4.func D() { x := bytes.Buffer{}; x.WriteString("hi"); fmt.Println(x.String()) }5.func E(s []int) int { return len(s) }called asE(make([]int, 8)).
6.func F() any { return 7 }(boxing intointerface{}`).
7. A method call on an interface value vs the concrete type (covered in Week 7).
For each that escapes, propose a refactor that keeps it on the stack. Then write a Criterion-style benchmark (testing.B) and prove the win.
4.4 Idiomatic & golangci-lint Drill¶
staticcheck SA6002(sync.Poolaccepting non-pointer types-silent allocation),gocritic: hugeParam,prealloc,makezero. Each maps to an allocation pathology.
4.5 Production Hardening Slice¶
- Configure
golangci-lintto fail on new escape-related issues introduced by a PR. Add a CI step that runsgo test -bench=. -benchmemon critical packages and diffs allocations against a baseline (benchstat).
Month 1 Capstone Deliverable¶
A workspace runtime-foundations/ with three modules:
1. schedule-forensics (week 2 lab)-produces a labeled trace.out and a markdown latency-distribution report.
2. stack-growth (week 3 lab)-produces a graph of StackInuse over time.
3. escape-clinic (week 4 lab)-six benchmarks with before / after allocation counts.
CI must run: gofmt -l, go vet, golangci-lint, go test -race, go test -bench=. -benchmem | tee bench.txt, benchstat baseline.txt bench.txt. The baseline is captured at week 4's end and tracked from then on.