從記憶體分配策略(堆、棧)的角度分析,函式傳遞指標真的比傳值效率高嗎?

_西門吹牛發表於2019-03-19

從記憶體分配策略(堆、棧)的角度分析,函式傳遞指標真的比傳值效率高嗎?

持續更新於我的 Github ,歡迎 Star

介紹

對於初學者,肯定很多同學在糾結:

  • 函式傳遞指標還是傳值?
  • 兩種選擇的本質區別是什麼?
  • 哪種方式的效能更高呢?

分析

要找到區別,那肯定需要下功夫,那就從 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方法)
  • 閉包引用物件逃逸,其實本質還是共享了棧上的值

相關文章