您好,登錄后才能下訂單哦!
這篇文章主要介紹“Go語言中怎么對棧中函數進行內聯”,在日常操作中,相信很多人在Go語言中怎么對棧中函數進行內聯問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Go語言中怎么對棧中函數進行內聯”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
把函數內聯到它的調用處消除了調用的開銷,為編譯器進行其他的優化提供了更好的機會,那么問題來了,既然內聯這么好,內聯得越多開銷就越少,為什么不盡可能多地內聯呢?
內聯可能會以增加程序大小換來更快的執行時間。限制內聯的最主要原因是,創建許多函數的內聯副本會增加編譯時間,并導致生成更大的二進制文件的邊際效應。即使把內聯帶來的進一步的優化機會考慮在內,太激進的內聯也可能會增加生成的二進制文件的大小和編譯時間。
內聯收益最大的是小函數,相對于調用它們的開銷來說,這些函數做很少的工作。隨著函數大小的增長,函數內部做的工作與函數調用的開銷相比省下的時間越來越少。函數越大通常越復雜,因此優化其內聯形式相對于原地優化的好處會減少。
在編譯過程中,每個函數的內聯能力是用內聯預算計算的 1。開銷的計算過程可以巧妙地內化,像一元和二元等簡單操作,在抽象語法數(AST)中通常是每個節點一個單位,更復雜的操作如 make
可能單位更多。考慮下面的例子:
package main func small() string { s := "hello, " + "world!" return s} func large() string { s := "a" s += "b" s += "c" s += "d" s += "e" s += "f" s += "g" s += "h" s += "i" s += "j" s += "k" s += "l" s += "m" s += "n" s += "o" s += "p" s += "q" s += "r" s += "s" s += "t" s += "u" s += "v" s += "w" s += "x" s += "y" s += "z" return s} func main() { small() large()}
使用 -gcflags=-m=2
參數編譯這個函數能讓我們看到編譯器分配給每個函數的開銷:
% go build -gcflags=-m=2 inl.go# command-line-arguments./inl.go:3:6: can inline small with cost 7 as: func() string { s := "hello, world!"; return s }./inl.go:8:6: cannot inline large: function too complex: cost 82 exceeds budget 80./inl.go:38:6: can inline main with cost 68 as: func() { small(); large() }./inl.go:39:7: inlining call to small func() string { s := "hello, world!"; return s }
編譯器根據函數 func small()
的開銷(7)決定可以對它內聯,而 func large()
的開銷太大,編譯器決定不進行內聯。func main()
被標記為適合內聯的,分配了 68 的開銷;其中 small
占用 7,調用 small
函數占用 57,剩余的(4)是它自己的開銷。
可以用 -gcflag=-l
參數控制內聯預算的等級。下面是可使用的值:
-gcflags=-l=0
默認的內聯等級。
-gcflags=-l
(或 -gcflags=-l=1
)取消內聯。
-gcflags=-l=2
和 -gcflags=-l=3
現在已經不使用了。和 -gcflags=-l=0
相比沒有區別。
-gcflags=-l=4
減少非葉子函數和通過接口調用的函數的開銷。2
一些函數雖然內聯的開銷很小,但由于太復雜它們仍不適合進行內聯。這就是函數的不確定性,因為一些操作的語義在內聯后很難去推導,如 recover
、break
。其他的操作,如 select
和 go
涉及運行時的協調,因此內聯后引入的額外的開銷不能抵消內聯帶來的收益。
不確定的語句也包括 for
和 range
,這些語句不一定開銷很大,但目前為止還沒有對它們進行優化。
在過去,Go 編譯器只對葉子函數進行內聯 —— 只有那些不調用其他函數的函數才有資格。在上一段不確定的語句的探討內容中,一次函數調用就會讓這個函數失去內聯的資格。
進入棧中進行內聯,就像它的名字一樣,能內聯在函數調用棧中間的函數,不需要先讓它下面的所有的函數都被標記為有資格內聯的。棧中內聯是 David Lazar 在 Go 1.9 中引入的,并在隨后的版本中做了改進。這篇文稿深入探究了保留棧追蹤行為和被深度內聯后的代碼路徑里的 runtime.Callers
的難點。
在前面的例子中我們看到了棧中函數內聯。內聯后,func main()
包含了 func small()
的函數體和對 func large()
的一次調用,因此它被判定為非葉子函數。在過去,這會阻止它被繼續內聯,雖然它的聯合開銷小于內聯預算。
棧中內聯的最主要的應用案例就是減少貫穿函數調用棧的開銷。考慮下面的例子:
package main import ( "fmt" "strconv") type Rectangle struct {} //go:noinlinefunc (r *Rectangle) Height() int { h, _ := strconv.ParseInt("7", 10, 0) return int(h)} func (r *Rectangle) Width() int { return 6} func (r *Rectangle) Area() int { return r.Height() * r.Width() } func main() { var r Rectangle fmt.Println(r.Area())}
在這個例子中, r.Area()
是個簡單的函數,調用了兩個函數。r.Width()
可以被內聯,r.Height()
這里用 //go:noinline
指令標注了,不能被內聯。3
% go build -gcflags='-m=2' square.go # command-line-arguments./square.go:12:6: cannot inline (*Rectangle).Height: marked go:noinline ./square.go:17:6: can inline (*Rectangle).Width with cost 2 as: method(*Rectangle) func() int { return 6 }./square.go:21:6: can inline (*Rectangle).Area with cost 67 as: method(*Rectangle) func() int { return r.Height() * r.Width() } ./square.go:21:61: inlining call to (*Rectangle).Width method(*Rectangle) func() int { return 6 } ./square.go:23:6: cannot inline main: function too complex: cost 150 exceeds budget 80 ./square.go:25:20: inlining call to (*Rectangle).Area method(*Rectangle) func() int { return r.Height() * r.Width() }./square.go:25:20: inlining call to (*Rectangle).Width method(*Rectangle) func() int { return 6 }
由于 r.Area()
中的乘法與調用它的開銷相比并不大,因此內聯它的表達式是純收益,即使它的調用的下游 r.Height()
仍是沒有內聯資格的。
關于棧中內聯的效果最令人吃驚的例子是 2019 年 Carlo Alberto Ferraris 通過允許把 sync.Mutex.Lock()
的快速路徑(非競爭的情況)內聯到它的調用方來提升它的性能。在這個修改之前,sync.Mutex.Lock()
是個很大的函數,包含很多難以理解的條件,使得它沒有資格被內聯。即使鎖可用時,調用者也要付出調用 sync.Mutex.Lock()
的代價。
Carlo 把 sync.Mutex.Lock()
分成了兩個函數(他自己稱為外聯)。外部的 sync.Mutex.Lock()
方法現在調用 sync/atomic.CompareAndSwapInt32()
且如果 CAS(比較并交換)成功了之后立即返回給調用者。如果 CAS 失敗,函數會走到 sync.Mutex.lockSlow()
慢速路徑,需要對鎖進行注冊,暫停 goroutine。4
% go build -gcflags='-m=2 -l=0' sync 2>&1 | grep '(*Mutex).Lock'../go/src/sync/mutex.go:72:6: can inline (*Mutex).Lock with cost 69 as: method(*Mutex) func() { if "sync/atomic".CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { }; return }; m.lockSlow() }
通過把函數分割成一個簡單的不能再被分割的外部函數,和(如果沒走到外部函數就走到的)一個處理慢速路徑的復雜的內部函數,Carlo 組合了棧中函數內聯和編譯器對基礎操作的支持,減少了非競爭鎖 14% 的開銷。之后他在 sync.RWMutex.Unlock()
重復這個技巧,節省了另外 9% 的開銷。
到此,關于“Go語言中怎么對棧中函數進行內聯”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。