Go Runtime Performance
Lesson, slides, and applied problem sets.
View SlidesLesson
Go Runtime Performance: Allocations, Scheduler, GC, and Tuning
This module is the engine room for performance work in this pack. Before optimizing code, establish a narrow scope and a measurement contract.
1) Build a performance hypothesis first
Treat each benchmark as an experiment:
- Reproduce the path with a deterministic harness.
- Measure with
-benchmem. - Form 1–3 hypotheses.
- Change one variable only.
- Re-measure before and after.
2) Performance contract and data collection
Use this minimum command set for every hot loop candidate:
go test -run none -bench . -benchmemallocs/op: allocation countns/op: latencyB/op: alloc bytes
go test -run none -bench BenchmarkX -cpuprofile cpu.out -memprofile mem.out- CPU for algorithmic waste
- mem profile for hot allocation callsites
go test -run none -bench BenchmarkX -trace trace.out && go tool trace trace.out- goroutine scheduling, network stalls, mutex contention
GODEBUG=gctrace=1during load tests- GC cadence, pause behavior, heap growth
3) Allocation control: where runtime cost hides
- Converting between
stringand[]bytecreates copies. fmtand JSON paths often allocate internally.appendwith insufficient capacity allocates and copies.- Maps grow aggressively under poor preallocation.
- Interface values in hot loops can force boxing and escape.
Rules that work:
- Pre-size slices and maps where you can.
- Prefer
[]bytereaders/writers over string concatenation in hot parsing paths. - Reuse caller-owned buffers when protocol shape is bounded.
- Measure allocations at leaf functions before changing architecture.
4) Escape analysis as a first design signal
If a value escapes to heap unexpectedly, inspect function shape.
Run: go build -gcflags=all=-m ./...
Interpret:
escapes to heapon a value that should be stack-bound suggests closure capture, interface conversion, or oversized slices.- Reduce scope, avoid returning oversized structs by value, and keep scratch buffers owned by callers.
5) Garbage collection realities
GC cost is mostly driven by:
- allocation rate,
- amount of live heap,
- pointer density.
Practical implications:
- shrinking live set often beats shaving a few allocation sites,
- long-lived object graphs are expensive even when request throughput looks healthy,
- pointer-heavy structures increase scan work.
Tuning knobs:
GOGC: lower values reclaim more aggressively; higher values reduce GC frequency at memory cost.GOMEMLIMIT: hard cap that nudges GC behavior when memory is near limit.
6) Scheduler awareness: M, P, G mental model
G: goroutines (logical tasks)P: processors (logical schedulers, usuallyGOMAXPROCS)M: OS threads
Rules:
- Goroutines are cheap, but not free.
- CPU-bound fan-out usually benefits from bounded worker pools.
- I/O-bound fan-out can keep one goroutine per connection in many cases.
- Measure run queue saturation before over-splitting work.
7) Lock strategy for throughput
- If data is mostly read, prefer immutable snapshots with synchronization for updates.
- If writes are frequent, prefer sharding instead of one global lock.
- For bursty writes, batch updates under one lock or one mutex-held section.
- Never assume one lock is best without scheduler+mutex profiling.
8) Memory model and concurrency correctness for performance work
No benchmark result is valid without defined synchronization.
sync.Mutex,sync/atomic, and channel close semantics provide happens-before edges.- Data races can create phantom performance gains that vanish in production.
- In suspected lock-free or lock-reduced sections, run
-raceonce behavior is stable.
9) Generics: when it helps and when it does not
Generics are not automatic speedups.
- Use generics to remove duplication and keep APIs clean.
- Measure generic vs specialized variants for hot paths.
- Avoid returning
anyfrom hot generic helpers; it can force boxing.
Use constraints intentionally:
- Tight constraints (
constraints.Integer) keep compile-time specialization. - Avoid over-broad constraints when only numeric operations are needed.
10) Practical playbook (now apply)
For each function in this pack:
- Establish a strict contract (
allocs == 0, max concurrency, order guarantees). - Pick one bottleneck from profiling output.
- Apply one targeted change.
- Re-run the same measurement command.
The strongest signal is a reproducible improvement under the same workload.
Module Items
Zero-Alloc HTTP Server
Byte Arena