從記憶體分配策略(堆、棧)的角度分析,函式傳遞指標真的比傳值效率高嗎?
介紹
對於初學者,肯定很多同學在糾結:
- 函式傳遞指標還是傳值?
- 兩種選擇的本質區別是什麼?
- 哪種方式的效能更高呢?
分析
要找到區別,那肯定需要下功夫,那就從 Golang 的實現機制中來分析吧。首先,在Golang 中有一個很重要的概念那就是 逃逸分析(Escape analysis),所謂的逃逸分析指由編譯器決定記憶體分配的位置。
- 分配在 棧中,則函式執行結束可自動將記憶體回收
- 分配在 堆中,則函式執行結束可交給GC(垃圾回收)處理
最終程式的執行效率和這個兩種分配規則是有這重要關聯的,而傳值和傳指標的主要區別在於底層值是否需要拷貝,表面上看傳指標不涉及值拷貝,效率肯定更高。但是實際情況是指傳針會涉及到變數逃逸到堆上,而且會增加GC的負擔,所以本文我們要做的內容就是進行 逃逸分析 ,安裝慣例先上結論。
- 棧上分配記憶體比在堆中分配記憶體有更高的效率
- 棧上分配的記憶體不需要GC處理,函式執行後自動回收
- 堆上分配的記憶體使用完畢會交給GC處理
- 發生逃逸時,會把棧上的申請的記憶體移動到堆上
- 指標可以減少底層值的拷貝,可以提高效率,但是會產生逃逸,但是如果拷貝的資料量小,逃逸造成的負擔(堆記憶體分配+GC回收)會降低效率
- 因此選擇值傳遞還是指標傳遞,變數的大小是一個很重要的分析指標
每種方式都有各自的優缺點,棧上的值,減少了 GC 的壓力,但是要維護多個副本,堆上的指標,會增加 GC 的壓力,但只需維護一個值。因此選擇哪種方式,依據自己的業務情況參考這個標準進行選擇。
先上一段程式碼分析下
// escape.go
package main
type person struct {
name string
age int
}
func main() {
makePerson(32, "艾瑪·斯通")
showPerson(33, "楊冪")
}
func makePerson(age int, name string) *person {
maliya := person{name, age}
return &maliya
}
func showPerson(age int, name string) person {
yangmi := person{name, age}
return yangmi
}
複製程式碼
執行如下命令,進行逃逸分析
go build -gcflags="-m -m -l" escape.go
複製程式碼
輸出結果:
Escape/Escape.go:15:9: &maliya escapes to heap
Escape/Escape.go:15:9: from ~r2 (return) at Escape/Escape.go:15:2
Escape/Escape.go:14:2: moved to heap: maliya
Escape/Escape.go:13:40: leaking param: name to result ~r2 level=-1
Escape/Escape.go:13:40: from person literal (struct literal element) at Escape/Escape.go:14:18
Escape/Escape.go:13:40: from maliya (assigned) at Escape/Escape.go:14:9
Escape/Escape.go:13:40: from &maliya (address-of) at Escape/Escape.go:15:9
Escape/Escape.go:13:40: from ~r2 (return) at Escape/Escape.go:15:2
Escape/Escape.go:18:39: leaking param: name to result ~r2 level=0
Escape/Escape.go:18:39: from person literal (struct literal element) at Escape/Escape.go:19:18
Escape/Escape.go:18:39: from yangmi (assigned) at Escape/Escape.go:19:9
Escape/Escape.go:18:39: from ~r2 (return) at Escape/Escape.go:20:2
複製程式碼
從結果中我們看到變數 &maliya 發生了逃逸,變數 yangmi 沒有逃逸
&maliya escapes to heap from ~r2 (return) at Escape/Escape.go:15:2
moved to heap: maliya
複製程式碼
所以 makePerson 返回的是指標型別,發生了逃逸,而showPerson 返回的是值型別沒有逃逸。
關於變數逃逸的情況還有很多,網上有很多分析的文章,就不一一舉例了,直接給出結論:
- 共享了棧上的一個值時,它就會逃逸
- 棧空間不足逃逸(比如建立一個超大的slice,超過棧空間)
- 動態型別逃逸,函式引數為interface型別(典型的fmt.Println方法)
- 閉包引用物件逃逸,其實本質還是共享了棧上的值