Not relying on inlining

Inlining is replacing a function call with the body of the function, usually done automatically by the compiler. In Go, this can be observed by running go build -gcflags. This only works if the function falls within the inlining budget, otherwise the function is too complex to be inlined.

Inlining offers two benefits: removing the function call overhead and enabling more compiler optimizations. Mid-stack Inlining is inlining functions that call other functions. This is useful because it allows for fast-path inlining, distinguishing between fast and slow paths.

Mistake

There are clearly two primary paths in this function. The mutex isn’t locked (fast) and the mutex is locked (slow).

func (m *Mutex) Lock() {
  if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    // mutex is not locked
    if race.Enabled {
      race.Acquire(unsafe.Pointer(m))
    }
    return
  }

  // mutex is already locked
  var waitStartTime int64
  starving := false
  awoke := false
  iter := 0
  for {
    // complicated logic
  }

  if race.Enabled {
    race.Acquire(unsafe.Pointer(m))
  }
}

Fix

The Lock() method can now be inlined.

func (m *Mutex) Lock() {
  if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    // mutex is not locked
    if race.Enabled {
      race.Acquire(unsafe.Pointer(m))
    }
    return
  }
  m.lockSlow()
}

func (m *Mutex) lockSlow() {
  // mutex is already locked
  var waitStartTime int64
  starving := false
  awoke := false
  iter := 0
  for {
    // complicated logic
  }

  if race.Enabled {
    race.Acquire(unsafe.Pointer(m))
  }
}

References