在Go中使用接口(interface{})好像有性能問題,來看一個例子:跑了三個benchmark,一個是接口調用,一個是直接使用,后面又加了一個接口斷言后調用
lib_test.go
package main import "testing" type D interface { Append(D) } type Strings []string func (s Strings) Append(d D) { } func BenchmarkInterface(b *testing.B) { s := D(Strings{}) for i := 0; i < b.N; i++ { s.Append(Strings{""}) } } func BenchmarkConcrete(b *testing.B) { s := Strings{} for i := 0; i < b.N; i++ { s.Append(Strings{""}) } } func BenchmarkInterfaceTypeAssert(b *testing.B) { s := D(Strings{}) for i := 0; i < b.N; i++ { s.(Strings).Append(Strings{""}) } }
運行:go test -bench=. -benchmem -run=none
可以看到直接使用接口調用確實效率比直接調用低了很多,但是,當我們將類型斷言之后,可以發現這個效率基本沒有差別,這是為什么呢?答案是內聯和內存逃逸!
內聯inline
什么是內聯,內聯是一個基本的編譯器優化,它用被調用函數的主體替換函數調用,以消除調用開銷,但更重要的是啟用了其他編譯器優化,這是在編譯過程中自動執行的一類基本優化之一。它對於我們程序性能提升主要有兩方面:
1.消除了函數調用本身的開銷
2.允許編譯器更有效地應用其他優化策略(例如常量折疊,公共子表達式消除,循環不變代碼移動和更好的寄存器分配)
可以通過一個例子直觀看一下內聯的作用:
package main import "testing" //go:noinline func max(a, b int) int { if a >b { return a } return b } var Result int func BenchmarkMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { r = max(-1, i) } Result = r }
執行:go test -bench=. -benchmem -run=none
然后允許max函數內聯,也就是把 //go:noinline 這行代碼刪除,再執行一遍:
對比使用內聯的前后,我們可以看到性能有極大的提升:2.18 ns/op -> 0.500 ns/op
內聯做了什么
首先,減少了相關函數的調用,將max的內容嵌入調用方減少了處理器執行指令的數量,消除了調用分支。
由於 r = max(-1, i) ,i 是從0開始的,所以 i > -1 ,那么 max 函數的 a > b 分支永遠不會發生。編譯器可以把這部分代碼直接內聯 至調用方,優化后的代碼:
func BenchmarkMax(b *testing.B) { var r int for i := 0; i < b.N; i++ { if -1 > i { r = -1 } else { r = i } } Result = r }
上面討論的這種情況是葉子內聯,將調用棧底部的函數內兩到直接調用方的行為。內聯是一個遞歸的過程,一旦函數被內聯到其調用方,編譯器就可以將結果代碼嵌入至調用方,以此類推。
內聯的限制
並不是任何函數都是可以內聯的,僅能內聯簡短和簡單的函數。要內聯,函數必須包含少於 40 個表達式,並且不包含復雜的語句,例如: loop, label, closure, panic, recover, select, switch 等。
堆棧中間內聯 mid - stack
Go1.8開始,編譯器默認不內聯堆棧中間(mid - stack)函數(即調用了其他不可內聯的函數)。堆棧中間內聯經過壓力測試可以將性能提高9%,帶來的副作用是編譯的二進制文件大小會增加15%。
逃逸分析
什么是內存逃逸?首先我們知道,內存分為堆內存(heap)和棧內存(stack)。對於堆內存來說,是需要清理的。堆上沒有被指針引用的值都需要刪除。隨着檢查和刪除的值越多,GC每次執行的工作就越多。
如果一個函數返回對一個變量的引用,那么它就會發生逃逸。因為在別的地方會引用這個變量,如果放在棧離里,函數退出后,內存就被回收了,所以需要逃逸到堆上。
簡而言之,逃逸分析決定了內存被分配到棧上還是堆上。
可以通過查看編譯器的報告來了解是否發生了內存逃逸。使用 go build -gcflags=-m 即可。總共有4個級別的 -m , 但是超過2個 -m 級別的返回的信息比較多。通常使用2個 -m 級別。
接口類型的方法調用
go中的接口類型的方法調用時動態調度,因此不能夠在編譯階段確定,所有類型結構轉換成接口的過程會涉及到內存逃逸的情況發生。
package main type S struct { s1 int } func (s *S) M1(i int) { s.s1 = i } type I interface { M1(int) } func g() { var s1 S var s2 S var s3 S f1(&s1) f2(&s2) f3(&s3) } func f1(s I) { s.M1(42) } func f2(s *S) { s.M1(42) } func f3(s I) { s.(*S).M1(42) }
執行 go build -gcflags=-m channleDemo1.go
可以看到接口方法調用不能內聯,而斷言和具體類型調用可以繼續內聯,直接接口方法調用,會發生內存逃逸。