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(增刪改查)

我要說說
網上賓友點評