THE MID-STACK INLINER
直译为“中栈内联”,属于一种更为新进的内联策略,我看有些地方也叫“栈中内联”。内联(InLining)的工作原理是将对一个函数的调用展开为函数本身的代码,通过内联减少函数调用的开销,也给编译器带来进一步优化代码的机会。那么,通过内联具体可以节省哪些调用开销呢?
Go编译器会基于一套复杂的规则和模型来决定是否要对函数做内联,这个过程是Go编译时自动帮我们完成的。但作为代码开发者的我们,有没有一种编程的方式方法,主动引导编译器对某些函数进行内联优化呢?
文章的内容主要参考 ,Go版本环境为 1.21.9,以一种更简单的方式来深入了解内联优化。建议你可以跳转阅读原文,本篇可以让你有更加深入的理解。
什么是内联?内联替换了函数调用的部分,将函数体直接展开在被调用的函数中,比较明显,这样做其实节省了一次函数调用的开销,并允许编译器做进一步优化。
但这种优化其实是编译器自动应用的,我们作为开发人员没法干涉这个过程,但我们其实可以干涉中栈内联。
mid-stack 表示的就是调用,之前的Go版本不会支持这种内联情况。但 ppt 给我们解释了,go是如何支持上这种能力的,并且在性能上带来了9%的提升。
下面的命令可以显示代码内联的情况,以及变量的逃逸情况。看起来这个命令能给我们很多性能的分析情况。关于它的参数 gcflags 也非常丰富,丰富到搞不懂它具体能干什么。
下面的例子中只出现了一个 -m,其实你可以指定多个 -m,中间用可空格分割,更多的 -m 会输出更加详细的信息,但也会使输出信息更加复杂,复杂到完全看不懂。
go build -gcflags '-m' main.go
作为合格的的蚂蚁,还是要做到浅尝辄止的,我们试图去看看命令输出的信息。它会输出一套比较复杂的评估内联预算的逻辑。这需要有些示例代码做支持,下面是代码示例:
仔细看下面的代码,对于函数 GetEven 的封装有时候会有不同的意见,探究这层封装的必要性。写这行代码的人说:知我者谓我心忧,不知我者谓我何求。
我对下面代码依次执行 -m 和 -m -m 的区别。
package main
import "fmt"
func main() {
demo := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 18, 19, 20}
out := GetEven(demo)
fmt.Println(out)
}
func GetEven(input []int) []int {
dest := make([]int, 0, len(input))
return getEven(input, dest)
}
func getEven(input []int, dest []int) []int {
for _, v := range input {
if v%2 == 0 {
dest = append(dest, v)
}
}
return dest
}
下面是 -m 的输出结果,可以看出 getEven 函数被内联调用了。虽然我们做了 getEven 的函数封装,但其实没有函数调用的开销,编译器识别出这里的函数嵌套复杂度太低了。
了解这些,可能会对未来的分析有帮助。分析代码能做到有理有据,不至于把屁股当脑袋来使用
我们做到有理有据,把官方的代码贴出来,这种中栈内联的设计,大牛还是门清门清的。用来说明的是 sync.Mutex
下面的代码有两个注释部分:Fast path 和 Slow path。只要代码的复杂度不超过预算,代码就会是内联的后选者。现在的代码也是在不断的迭代过程中优化成这个样子的。
给我们的启发就是:代码的封装,或者说,接下来要说的 API 设计,通过合理的设计,其实可以同时成就代码的可读性和性能。
// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
很常见的一个问题:虽然实现的是同一个功能,但不同的开发者有不同的写法(每种不同的写法,都是一种API设计)。下面从两个角度来设计实现:从整数类型的切片中获取偶数的功能
这种模式相对比较常见,在函数内部声明一个空的切片类型变量,将偶数追加到这个变量中,最后返回,代码示例如下截图。代码中的 even
就是声明的新变量,最终作为返回值返回。
因篇幅问题不能全部显示,请点此查看更多更全内容