Go Runtime Performance

Lesson, slides, and applied problem sets.

View Slides

Lesson

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:

  1. Reproduce the path with a deterministic harness.
  2. Measure with -benchmem.
  3. Form 1–3 hypotheses.
  4. Change one variable only.
  5. 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 . -benchmem
    • allocs/op: allocation count
    • ns/op: latency
    • B/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=1 during load tests
    • GC cadence, pause behavior, heap growth

3) Allocation control: where runtime cost hides

  • Converting between string and []byte creates copies.
  • fmt and JSON paths often allocate internally.
  • append with 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 []byte readers/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 heap on 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, usually GOMAXPROCS)
  • 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 -race once 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 any from 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:

  1. Establish a strict contract (allocs == 0, max concurrency, order guarantees).
  2. Pick one bottleneck from profiling output.
  3. Apply one targeted change.
  4. Re-run the same measurement command.

The strongest signal is a reproducible improvement under the same workload.


Module Items

  • Zero-Alloc HTTP Server

    Parse a request and write a response with zero allocations.

    hard Upgrade to Pro to access hard problems
  • Byte Arena

    Implement a bump allocator for reusable byte slices.

    medium Sign in to access medium and hard problems
Join Discord