問題:
fmt.Printf等函式會導致傳進去的引數在編譯時從堆疊逃逸到堆上?
golang的issue:(https://github.com/golang/go/issues/8618)
磨刀(逃逸分析工具):
分析工具:
- 1.通過編譯工具查看詳細的逃逸分析程序(
go build -gcflags '-m -l' main.go) - 2.通過反編譯命令查看
go tool compile -S main.go
其中 編譯引數(-gcflags)介紹:
-
-N: 禁止編譯優化 -
-l: 禁止行內(可以有效減少程式大小) -
-m: 逃逸分析(最多可重復四次) -
-benchmem: 壓測時列印記憶體分配統計
練功(實驗):
Example:
package main
import (
"fmt"
"runtime"
)
type obj struct{}
func main() {
a := &obj{}
fmt.Printf("%p\n", a)
b := &obj{}
println(b)
}
逃逸分析:
./main.go:17:7: &obj literal escapes to heap
./main.go:18:12: ... argument does not escape
./main.go:20:7: &obj literal does not escape
0x11a6c10
0xc000072f1f
可以看到我們的變數a因為fmt的函式逃逸到了堆上,
干仗(嘗試驗證問題):
首先宣告,我們只是驗證了這個問題的發生,但并沒有解決這個問題,有想法的同學可以直接去提交mr(https://github.com/golang/go/issues/8618)
目前網上的解釋有2個方向:
fmt.Printf等函式會導致傳進去的引數在編譯時從堆疊逃逸到堆上
第一個罪犯:
其實,fmt.Printf 的第二個引數,是一個 interface 型別,在底層的呼叫中用到了斷言,具體的呼叫邏輯是:
Printf->Fprintf->doPrintf->reflect.TypeOf(arg).Kind()
這里有人通過模擬fmt包得出了初步的結論(https://reusee.github.io/post/escape_analysis/),呼叫interface的Type方法會導致變數被移到堆上,
所以我們可以認為a在編譯階段,編譯器無法確定其具體的型別,因此會產生逃逸,最終分配到堆上(最本質的原因是interface{}型別一般情況下底層會進行reflect,而使用的reflect.TypeOf(arg).Kind()獲取介面型別物件的底層資料型別時發生了堆逃逸,最終就會反映為當入參是空介面型別時發生了逃逸),
但不是說往func(interface{})傳值,或者往func(*struct)傳指標就會導致逃逸分析,只是大多數場景下,其內部都會用到反射,導致逃逸(switch type不會導致逃逸),
驗證:
package main
import (
"fmt"
"runtime"
)
type obj struct{}
func main() {
a := &obj{}
fmt.Printf("%p\n", a)
b := &obj{}
reflect.TypeOf(b).Kind()
println(b)
}
逃逸分析:
# command-line-arguments
./main.go:20:7: &obj literal escapes to heap
./main.go:21:12: ... argument does not escape
./main.go:23:7: &obj literal escapes to heap
0x11a6c30
0x11a6c30
可以發現兩個變數都到了堆上,至于地址為什么一摸一樣,可以關注我的另一篇文章:https://www.jianshu.com/p/e0fd84a59088
第二個罪犯:
我們點進去看看fmt.Printf的原始碼,同樣的排查鏈路,但是在上一個原因前,我們返現有這么一段:
我們傳入的u被賦值給了pp指標的一個成員變數:
func (p *pp) printArg(arg interface{}, verb rune) { p.arg = arg p.value = reflect.Value{} ... }
而這個pp型別的指標p是由建構式newPrinter回傳的,所以他的壽命范圍就變了,p一定發生逃逸,而p參考了傳入指標u,經測驗是逃逸了,
具體的驗證可看這篇文章(https://hansedong.github.io/2019/04/03/16/)
收功(總結):
fmt.Printf等函式傳入引數會發生堆逃逸,
雖然日常的開發,go已經幫我們處理了編譯前的記憶體分配,我們也不需要關注堆疊的使用情況,但有意識的避免堆逃逸可以有效的提高負擔重的服務性能,堆逃逸在go中并不罕見,并且對gc的影響帶來的性能消耗也是不容小覷的,
日常經常會碰到的:
1.函式回傳指向堆疊內物件的指標,或者說是引數泄漏,延長了指標物件的生命周期,
2.呼叫反射(未知型別)(fmt案例的第一個問題),
3.被已經逃逸的變數參考的指標,一定發生逃逸(fmt案例的第二個問題),
4.被指標型別的slice、map和chan參考的指標,一定發生逃逸,
避免逃逸的好處:
-
1.減少gc的壓力,不逃逸的物件分配在堆疊上,當函式回傳時就回收了資源,不需要gc標記清除
-
2.逃逸分析完后可以確定哪些變數可以分配在堆疊上,堆疊的分配比堆快,性能好(系統開銷少)
-
3.減少動態分配所造成的記憶體碎片
反思:
函式傳遞指標真的比傳值效率高嗎?
我們知道傳遞指標可以減少底層值的拷貝,可以提高效率,負擔也比較小
但是當資料比較小且多的情況,由于指標傳遞經常會導致逃逸到堆上,會增加GC的負擔,所以傳遞指標不一定是高效的,
example:
使用指標的chan比使用值的chan慢30%,使用指標的chan發生逃逸,gc拖慢了速度,
ps :https://stackoverflow.com/questions/41178729/why-passing-pointers-to-channel-is-slower
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/280537.html
標籤:區塊鏈
