前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#97 Not relying on inlining

Go语言中常见100问题-#97 Not relying on inlining

作者头像
数据小冰
发布2024-02-01 17:04:29
1290
发布2024-02-01 17:04:29
举报
文章被收录于专栏:数据小冰数据小冰
利用内联

内联是指用函数体内容替换函数调用。内联过程是由编译器自动完成的,了解内联的基本原理有助于我们对一些场景下的代码进行优化。

先来看一个非常简单的例子,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 函数调用。

将简单逻辑处理和复杂逻辑处理区分开,如果简理逻辑处理可以被内联但是复杂逻辑处理不能被内联,我们可以将复杂处理部分提取到一个函数中,这样整体函数如果通过内联评估,在编译时就可以被内联处理。

所以函数内联不仅仅是编译器要关心的问题,作为开发者也需要关心,理解内联的工作机制可以有助于我们对程序进行优化,正如本文上面的例子,利用内联减少调用开销,提升程序运行速度。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 利用内联
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档