利用内聯
内聯是指用函數體内容替換函數調用。内聯過程是由編譯器自動完成的,了解内聯的基本原理有助於我們對一些場景下的代碼進行優化。
先來看一個非常簡單的例子,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 函數調用。
將簡單邏輯處理和複雜邏輯處理區分開,如果簡理邏輯處理可以被内聯但是複雜邏輯處理不能被内聯,我們可以將複雜處理部分提取到一個函數中,這樣整體函數如果通過内聯評估,在編譯時就可以被内聯處理。
所以函數内聯不僅僅是編譯器要關心的問題,作爲開發者也需要關心,理解内聯的工作機制可以有助於我們對程序進行優化,正如本文上面的例子,利用内聯減少調用開銷,提升程序運行速度。