go语言的Not relying on inlining

字号+ 编辑: 种花家 修订: 种花家 来源: 网络转载 2024-04-10 我要说两句(0)

讲讲go语言的内联。

利用内联

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

先来看一个非常简单的例子,sum是一个求和函数,完成两个数相加。

func main() {
    a := 3
    b := 2
    s := sum(a, b)
    println(s)
}
func sum(a, b int) int {
    return a + b
}

编译时使用 -gcflags ,可以输出编译器处理的详尽日志。在我的电脑上运行结果如下:

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函数中。上述代码内联后如下:

func main() {
    a := 3
    b := 2
    s := a + b
    println(s)
}

并不是任何函数都可以内联,内联只是对具有一定复杂性的函数有效,所以内联前要进行复杂性评估。如果函数太复杂,则不会内联,编译输出内容与下面类似。

./main.go:10:6: cannot inline foo: function too complex:
    cost 84 exceeds budget 80

函数内联后有两个收益,一是消除了函数调用的开销(尽管Go1.17版本基于寄存器的调用约定,相比之前开销已经有所减少);二是编译器可以进一步优化代码。例如,在函数被内联后,编译器可以决定最初应该在堆上逃逸的变量可以分配在栈上。

函数内联是编译器自动完成的,开发者有必要关心吗?需要关心,因为有中间栈内联。中间栈内联是调用其他函数的内联函数,在Go1.9之前,只有叶子函数(不会调用其它函数的函数)才会被内联。现在由于支持栈中内联,所以下面的foo函数也可以被内联。

func main(){
    foo()
}
func foo(){
    x:=1 bar(x)
}

内联后的代码如下:

func main() {
    x := 1 bar(x)
}

有了中间栈内联,在编写程序的时候,我们可以将快速路径(代码逻辑比较简单)内联达到优化程序目的。下面结合 sync.Mutex 的Lock实现,理解其原理。

在不支持中间栈内联之前,Lock方法实现如下:

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方法进行重构,将处理非常复杂的逻辑提取到一个特定的函数中。具体实现如下:

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 函数调用。

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

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


阅完此文,您的感想如何?
  • 有用

    42

  • 没用

    12

  • 开心

    1

  • 愤怒

    3

  • 可怜

    3

1.如文章侵犯了您的版权,请发邮件通知本站,该文章将在24小时内删除;
2.本站标注原创的文章,转发时烦请注明来源;
3.交流群: PHP+JS聊天群

相关课文
  • GO语言GORM如何更新字段

  • gorm如何创建记录与模型定义需要注意什么

  • gorm一般查询与高级查询

  • GORM时间戳跟踪及CURD(增删改查)

我要说说
网上宾友点评