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.交流群: 2702237 13835667

相關課文
  • GO語言GORM如何更新字段

  • gorm如何創建記錄與模型定義需要注意甚麼

  • gorm一般查詢與高級查詢

  • GORM時間戳跟蹤及CURD(增刪改查)

我要說說
網上賓友點評