Skip to content

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 &x for a local x.
  • Storing &x in a heap-allocated struct, slice, or map.
  • Capturing x by 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) boxes x).
  • 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:noinline and //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.out then go tool pprof -alloc_objects mem.out. The - alloc_objectsview counts allocations (escapes); - inuse_space counts 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.Pool accepting non-pointer types-silent allocation), gocritic: hugeParam, prealloc, makezero. Each maps to an allocation pathology.

4.5 Production Hardening Slice

  • Configure golangci-lint to fail on new escape-related issues introduced by a PR. Add a CI step that runs go test -bench=. -benchmem on 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.

Comments