利用内联
内联是指用函数体内容替换函数调用。内联过程是由编译器自动完成的,了解内联的基本原理有助于我们对一些场景下的代码进行优化。
先来看一个非常简单的例子,sum是一个求和函数,完成两个数相加。
代码语言:javascript复制func main() {
a := 3
b := 2
s := sum(a, b)
println(s)
}
func sum(a, b int) int {
return a b
}
编译时使用 -gcflags ,可以输出编译器处理的详尽日志。在我的电脑上运行结果如下:
代码语言:javascript复制go build -gcflags "-m=2"
# inline
./example1.go:10:6: can inline sum with cost 4 as: func(int, int) int { return a b }
./example1.go:3:6: can inline main with cost 24 as: func() { a := 3; b := 2; s := sum(a, b); println(s) }
./example1.go:6:10: inlining call to sum
编译器决定将sum函数内联到main函数中。上述代码内联后如下:
代码语言:javascript复制func main() {
a := 3
b := 2
s := a b
println(s)
}
并不是任何函数都可以内联,内联只是对具有一定复杂性的函数有效,所以内联前要进行复杂性评估。如果函数太复杂,则不会内联,编译输出内容与下面类似。
代码语言:javascript复制./main.go:10:6: cannot inline foo: function too complex:
cost 84 exceeds budget 80
函数内联后有两个收益,一是消除了函数调用的开销(尽管Go1.17版本基于寄存器的调用约定,相比之前开销已经有所减少);二是编译器可以进一步优化代码。例如,在函数被内联后,编译器可以决定最初应该在堆上逃逸的变量可以分配在栈上。
函数内联是编译器自动完成的,开发者有必要关心吗?需要关心,因为有中间栈内联。中间栈内联是调用其他函数的内联函数,在Go1.9之前,只有叶子函数(不会调用其它函数的函数)才会被内联。现在由于支持栈中内联,所以下面的foo函数也可以被内联。
代码语言:javascript复制func main(){
foo()
}
func foo(){
x:=1
bar(x)
}
内联后的代码如下:
代码语言:javascript复制func main() {
x := 1
bar(x)
}
有了中间栈内联,在编写程序的时候,我们可以将快速路径(代码逻辑比较简单)内联达到优化程序目的。下面结合 sync.Mutex 的Lock实现,理解其原理。
在不支持中间栈内联之前,Lock方法实现如下:
代码语言:javascript复制func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
// Mutex isn't locked
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Mutex is already locked
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// ...
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
整个Lock方法实现分为两种情况,如果互斥锁没有被锁定(即atomic.CompareAndSwapInt32为真),处理比较简单。如果互斥锁已经被锁定(即atomic.CompareAndSwapInt32为假),处理起来非常复杂。
然而,无论哪种情况,由于函数的复杂性,Lock都不能被内联。为了使用中间栈内联,对Lock方法进行重构,将处理非常复杂的逻辑提取到一个特定的函数中。具体实现如下:
代码语言:javascript复制func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
m.lockSlow()
}
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))
}
}
通过上面的优化,Lock函数复杂性降低,现在可以被内联。得到的收益是在互斥锁没有被锁定的情况下,没有函数调用开销(速度提高了5%左右)。在互斥锁已经被锁定的情况下没有变化,以前需要一个函数调用执行这个逻辑,现在仍然是一个函数调用,即 lockSlow 函数调用。
将简单逻辑处理和复杂逻辑处理区分开,如果简理逻辑处理可以被内联但是复杂逻辑处理不能被内联,我们可以将复杂处理部分提取到一个函数中,这样整体函数如果通过内联评估,在编译时就可以被内联处理。
所以函数内联不仅仅是编译器要关心的问题,作为开发者也需要关心,理解内联的工作机制可以有助于我们对程序进行优化,正如本文上面的例子,利用内联减少调用开销,提升程序运行速度。