liveBook · Manning

Performance optimization

As explained in 100 Go Mistakes and How to Avoid Them

A compiler optimization is a transformation that improves generated program code while preserving its required behavior.

A faster version is not automatically a better version. Keep code correct, clear, and concise first; optimize a path when its speed or efficiency matters. Identify the bottleneck before changing it, then check whether the change actually helps. Optimization can also target goals other than speed, such as floating-point accuracy. section 12.0 section 1.3.6

Common Go opportunities depend on the cause of the cost:

  • Reduce heap allocations: use strings.Builder for suitable concatenation, avoid unnecessary []byte-to-string conversions, preallocate slices and maps when their sizes are known, and consider struct alignment. section 12.6
  • Consider parallel execution when it is poorly structured, rather than assuming more concurrency will improve performance. section 1.3.6
  • Check whether function structure permits inlining; a complex slow path can prevent a frequently used fast path from being inlined. section 12.7

A benchmark that measures an empty loop

A benchmark calling popcnt(uint64(i)) without using its result reported 0.2858 ns/op. The compiler could inline the call and remove the side-effect-free work. Keep each result in a benchmark-local variable, then assign the final result to a global variable after the loop. That version reported 1.993 ns/op; writing to the global on every iteration would add avoidable benchmark overhead. section 11.8.3

Similar map lookups, different generated code

When a map has string keys but the lookup input is []byte, storing string(bytes) in a local key before lookup differs from writing m[string(bytes)] directly. The Go compiler has a specific optimization for the direct lookup that avoids the bytes-to-string conversion. Check generated behavior rather than assuming similar-looking source has the same cost. section 12.6.2

func (m *Mutex) Lock() {
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }
    m.lockSlow()     #1
}
 
func (m *Mutex) lockSlow() {
    var waitStartTime int64
    starving := false
    awoke := false
    iter := 0
    old := m.state
    for {
        // ...
    }
 
    if race.Enabled {
        race.Acquire(unsafe.Pointer(m))
    }
}

The refactored Mutex.Lock keeps the fast path in a small function and moves the complex slow path into lockSlow. This makes inlining the fast-path call possible under the compiler’s complexity budget. section 12.7

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Works through benchmark elimination, conversion avoidance, and inlining decisions, while directing bottleneck identification rather than prescribing universal rewrites.
Examples
Uses a population-count benchmark, a []byte-to-string map lookup, and a Mutex fast-path refactor.
What it assumes
Assumes familiarity with Go functions, slices, maps, and benchmarks; it does not teach their basic syntax.
What you can do after
Diagnose/debug with Go benchmarks and compiler inlining reports, then choose targeted code changes.
How it connects
Connects performance to heap allocations, benchmarking, compiler inlining, data alignment, and parallel execution.

Also explained in

Compare how 7 books explain performance optimization →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.