A common mistake is treating inlining as an invisible compiler detail. Fast-path optimization deliberately separates the common, cheap case from the uncommon, expensive case so the common case can be inlined. section 12.7
Mid-stack inlining lets the compiler inline functions that call other functions. If a function is too complex to inline as a whole, extract its slow path into a dedicated function and keep the fast path small enough to fit the inlining budget. section 12.7
Refactoring mutex locking
A mutex's unlocked case succeeds with an atomic compare-and-swap and returns immediately. The contended case previously lived in the same complex Lock method, preventing inlining. Moving that logic into lockSlow lets Lock be inlined: uncontended locking avoids a function-call overhead, while the slow path still pays one call. section 12.7
Apply the technique when:
- Keep the common path direct and compact, and move complex fallback logic into a separate function. section 12.7
- Check compiler diagnostics rather than assuming inlining occurred; excessive cost can exceed the compiler's budget. section 12.7
- Use profiling and the execution tracer to decide whether reducing call overhead matters for the application's workload. section 12.11
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))
}
}
Inspect the refactored Mutex implementation, where Lock retains the fast path and delegates contention to lockSlow. section 12.7
