什么網(wǎng)站系統(tǒng)做的最好的/磁力貓torrentkitty官網(wǎng)
“申請到棧內(nèi)存好處:函數(shù)返回直接釋放,不會引起垃圾回收,對性能沒有影響。
申請到堆上面的內(nèi)存才會引起垃圾回收。
func?F()?{
?a?:=?make([]int,?0,?20)
?b?:=?make([]int,?0,?20000)
?l?:=?20
?c?:=?make([]int,?0,?l)
}
“a和b代碼一樣,就是申請的空間不一樣大,但是它們兩個的命運(yùn)是截然相反的。a前面已經(jīng)介紹過,會申請到棧上面,而b,由于申請的內(nèi)存較大,編譯器會把這種申請內(nèi)存較大的變量轉(zhuǎn)移到堆上面。即使是臨時(shí)變量,申請過大也會在堆上面申請。
而c,對我們而言其含義和a是一致的,但是編譯器對于這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。
堆(Heap)和棧(Stack)
參考 此文[1] <內(nèi)存模型:Heap>, <內(nèi)存模型:Stack>部分的內(nèi)容:
Heap:
堆的一個重要特點(diǎn)就是不會自動消失,必須手動釋放,或者由垃圾回收機(jī)制來回收。
Stack:
棧是由于函數(shù)運(yùn)行而臨時(shí)占用的內(nèi)存區(qū)域
執(zhí)行main函數(shù)時(shí),會為它在內(nèi)存里面建立一個幀(frame),所有main的內(nèi)部變量(比如a和b)都保存在這個幀里面。main函數(shù)執(zhí)行結(jié)束后,該幀就會被回收,釋放所有的內(nèi)部變量,不再占用空間。
一般來說,調(diào)用棧有多少層,就有多少幀。
所有的幀都存放在 Stack,由于幀是一層層疊加的,所以 Stack 被翻譯為棧。 (棧這個字的原始含義,就有柵欄的意思,所謂 棧道,棧橋,都是指比較簡陋的用柵欄做的道路/橋梁)
即 在函數(shù)中申請一個新的對象:
如果分配 在棧中,則函數(shù)執(zhí)行結(jié)束可自動將內(nèi)存回收;不會引起垃圾回收,對性能沒有影響。
如果分配在堆中,則函數(shù)執(zhí)行結(jié)束可交給GC(垃圾回收)處理;如果這個過程(特指垃圾回收不斷被觸發(fā))過于高頻就會導(dǎo)致 gc 壓力過大,程序性能出問題。
C/C++中的new都是分配到堆上,Go則不一定(Java亦然)[2]
何為逃逸分析(Escape analysis)
在堆上分配的內(nèi)存,需要GC去回收,而在棧上分配,函數(shù)執(zhí)行完就銷毀,不存在垃圾回收的問題. 所以應(yīng)盡可能將內(nèi)存分配在棧上.
但問題是,對于一個函數(shù)或變量,并不能知道還有沒有其他地方在引用. 所謂的逃逸分析,就是為了確定這個事兒~
Go編譯器會跨越函數(shù)和包的邊界進(jìn)行全局的逃逸分析。它會檢查是否需要在堆上為一個變量分配內(nèi)存,還是說可以在棧本身的內(nèi)存里對其進(jìn)行管理。
何時(shí)發(fā)生逃逸分析?
“Go編譯器決定變量應(yīng)該分配到什么地方時(shí)會進(jìn)行逃逸分析
“From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.
However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
Q:如何得知變量是分配在棧(stack)上還是堆(heap)上?
A: 準(zhǔn)確地說,你并不需要知道。Golang 中的變量只要被引用就一直會存活,存儲在堆上還是棧上由內(nèi)部實(shí)現(xiàn)決定而和具體的語法沒有關(guān)系。
但知道變量的存儲位置確實(shí)對程序的效率有幫助。如果可能,Golang 編譯器會將函數(shù)的局部變量分配到函數(shù)棧幀(stack frame)上。然而,如果編譯器不能確保變量在函數(shù) return 之后不再被引用,編譯器就會將變量分配到堆上。而且,如果一個局部變量非常大,那么它也應(yīng)該被分配到堆上而不是棧上。 當(dāng)前情況下,如果一個變量被取地址,那么它就有可能被分配到堆上。然而,還要對這些變量做逃逸分析,如果函數(shù) return 之后,變量不再被引用,則將其分配到棧上。
可以使用go命令的 -gcflags="-m"
選項(xiàng),來觀察逃逸分析的結(jié)果以及GC工具鏈的內(nèi)聯(lián)決策[3] ([內(nèi)聯(lián)是一種手動或編譯器優(yōu)化,用于將簡短函數(shù)的調(diào)用替換為函數(shù)體本身。這么做的原因是它可以消除函數(shù)調(diào)用本身的開銷,也使得編譯器能更高效地執(zhí)行其他的優(yōu)化策略。我們可以顯式地在函數(shù)定義前面加一行//go:noinline
注釋讓編譯器不對函數(shù)進(jìn)行內(nèi)聯(lián))
實(shí)例
對于escape1.go代碼如下:
package?main
import?"fmt"
func?main()?{
?fmt.Println("Called?stackAnalysis",?stackAnalysis())
}
//go:noinline
func?stackAnalysis()?int?{
?data?:=?100
?return?data
}
通過 go build -gcflags "-m -l" escape1.go
go build -gcflags=-m escape1.go
來查看和分析逃逸分析:
./escape1.go:6:13:?inlining?call?to?fmt.Println
./escape1.go:6:14:?"Called?stackAnalysis"?escapes?to?heap
./escape1.go:6:51:?stackAnalysis()?escapes?to?heap
./escape1.go:6:13:?[]interface?{}{...}?does?not?escape
<autogenerated>:1:?.this?does?not?escape
escapes to heap 即代表該行該處 內(nèi)存分配發(fā)生了逃逸現(xiàn)象. 變量需要在函數(shù)棧之間共享(這個例子就是在main和fmt.Println之間在棧上共享)
-
第6行第13個字符處的字符串標(biāo)量"Called stackAnalysis"逃逸到堆上
-
第6行51個字符處的函數(shù)調(diào)用stackAnalysis()逃逸到了堆上
對于escape2.go代碼如下:
package?main
import?"fmt"
func?main()?{
?fmt.Println("Called?heapAnalysis",?heapAnalysis())
}
//go:noinline
func?heapAnalysis()?*int?{
?data?:=?100
?return?&data
}
執(zhí)行go build -gcflags=-m escape2.go
:
#?command-line-arguments
./escape2.go:6:13:?inlining?call?to?fmt.Println
./escape2.go:11:2:?moved?to?heap:?data
./escape2.go:6:14:?"Called?heapAnalysis"?escapes?to?heap
./escape2.go:6:13:?[]interface?{}{...}?does?not?escape
<autogenerated>:1:?.this?does?not?escape
函數(shù)heapAnalysis返回 int類型的指針,在main函數(shù)中會使用該指針變量. 因?yàn)槭窃趆eapAnalysis函數(shù)外部訪問,所以data變量必須被移動到堆上
主函數(shù)main會從堆中訪問該data變量
(可見指針雖能夠減少變量在函數(shù)間傳遞時(shí)的數(shù)據(jù)值拷貝,但不該所有類型數(shù)據(jù)都返回其指針.如果分配到堆上的共享變量太多會增加了GC的壓力)
逃逸類型
1. 指針逃逸:
對于 escape_a.go:
package?main
type?Student?struct?{
?Name?string
?Age??int
}
func?StudentRegister(name?string,?age?int)?*Student?{
?s?:=?new(Student)?//局部變量s逃逸到堆
?s.Name?=?name
?s.Age?=?age
?return?s
}
func?main()?{
?StudentRegister("dashen",?18)
}
執(zhí)行 go build -gcflags=-m escape_a.go
#?command-line-arguments
./escape_a.go:8:6:?can?inline?StudentRegister
./escape_a.go:17:6:?can?inline?main
./escape_a.go:18:17:?inlining?call?to?StudentRegister
./escape_a.go:8:22:?leaking?param:?name
./escape_a.go:9:10:?new(Student)?escapes?to?heap
./escape_a.go:18:17:?new(Student)?does?not?escape
**s 雖然為 函數(shù)StudentRegister()內(nèi)的局部變量, 其值通過函數(shù)返回值返回. 但s 本身為指針類型. 所以其指向的內(nèi)存地址不會是棧而是堆. **
這是一種典型的變量逃逸案例
2. ??臻g不足而導(dǎo)致的逃逸(空間開辟過大):
對于 escape_b.go:
package?main
func?InitSlice()?{
?s?:=?make([]int,?1000,?1000)
?for?index?:=?range?s?{
??s[index]?=?index
?}
}
func?main()?{
?InitSlice()
}
執(zhí)行go build -gcflags=-m escape_b.go
#?command-line-arguments
./escape_b.go:11:6:?can?inline?main
./escape_b.go:4:11:?make([]int,?1000,?1000)?does?not?escape
此時(shí)并沒有發(fā)生逃逸
將切片的容量增大10倍,即:
package?main
func?InitSlice()?{
?s?:=?make([]int,?1000,?10000)
?for?index?:=?range?s?{
??s[index]?=?index
?}
}
func?main()?{
?InitSlice()
}
執(zhí)行go build -gcflags=-m escape_b.go
#?command-line-arguments
./escape_b.go:11:6:?can?inline?main
./escape_b.go:4:11:?make([]int,?1000,?10000)?escapes?to?heap
發(fā)生了逃逸
當(dāng)棧空間不足以存放當(dāng)前對象,或無法判斷當(dāng)前切片長度時(shí),會將對象分配到堆中
ps:
package?main
func?InitSlice()?{
?s?:=?make([]int,?1000,?1000)
?for?index?:=?range?s?{
??s[index]?=?index
?}
?println(s)
}
func?main()?{
?InitSlice()
}
執(zhí)行go build -gcflags=-m escape_b.go
#?command-line-arguments
./escape_b.go:12:6:?can?inline?main
./escape_b.go:4:11:?make([]int,?1000,?1000)?does?not?escape
沒有逃逸.
而改成
package?main
import?"fmt"
func?InitSlice()?{
?s?:=?make([]int,?1000,?1000)
?for?index?:=?range?s?{
??s[index]?=?index
?}
?fmt.Println(s)
}
func?main()?{
?InitSlice()
}
執(zhí)行go build -gcflags=-m escape_b.go
,則
#?command-line-arguments
./escape_b.go:11:13:?inlining?call?to?fmt.Println
./escape_b.go:14:6:?can?inline?main
./escape_b.go:6:11:?make([]int,?1000,?1000)?escapes?to?heap
./escape_b.go:11:13:?s?escapes?to?heap
./escape_b.go:11:13:?[]interface?{}{...}?does?not?escape
<autogenerated>:1:?.this?does?not?escape
發(fā)生了逃逸
這是為何? 參見下文!
3. 動態(tài)類型逃逸(不確定長度大小):
當(dāng)函數(shù)參數(shù)為**interface{}**類型, 如最常用的fmt.Println(a …interface{})
, 編譯期間很難確定其參數(shù)的具體類型,也會產(chǎn)生逃逸
對于 escape_c1.go:
package?main
import?"fmt"
func?main()?{
?s?:=?"s會發(fā)生逃逸"
?fmt.Println(s)
}
執(zhí)行go build -gcflags=-m escape_c1.go
#?command-line-arguments
./escape_c1.go:7:13:?inlining?call?to?fmt.Println
./escape_c1.go:7:13:?s?escapes?to?heap
./escape_c1.go:7:13:?[]interface?{}{...}?does?not?escape
<autogenerated>:1:?.this?does?not?escape
對于 escape_c1.go:
package?main
func?main()?{
?InitSlice2()
}
func?InitSlice2()?{
?a?:=?make([]int,?0,?20)????//?棧?空間小
?b?:=?make([]int,?0,?20000)?//?堆?空間過大?逃逸
?l?:=?20
?c?:=?make([]int,?0,?l)?//?堆?動態(tài)分配不定空間?逃逸
?_,?_,?_?=?a,?b,?c
}
執(zhí)行go build -gcflags=-m escape_c2.go
#?command-line-arguments
./escape_c2.go:7:6:?can?inline?InitSlice2
./escape_c2.go:3:6:?can?inline?main
./escape_c2.go:4:12:?inlining?call?to?InitSlice2
./escape_c2.go:4:12:?make([]int,?0,?20)?does?not?escape
./escape_c2.go:4:12:?make([]int,?0,?20000)?escapes?to?heap
./escape_c2.go:4:12:?make([]int,?0,?l)?escapes?to?heap
./escape_c2.go:8:11:?make([]int,?0,?20)?does?not?escape
./escape_c2.go:9:11:?make([]int,?0,?20000)?escapes?to?heap
./escape_c2.go:12:11:?make([]int,?0,?l)?escapes?to?heap
4. 閉包引用對象逃逸:
對于如下斐波那契數(shù)列escape_d.go:
package?main
import?"fmt"
func?Fibonacci()?func()?int?{
?a,?b?:=?0,?1
?return?func()?int?{
??a,?b?=?b,?a+b
??return?a
?}
}
func?main()?{
?f?:=?Fibonacci()
?for?i?:=?0;?i?<?10;?i++?{
??fmt.Printf("Fibonacci:?%d\n",?f())
?}
}
執(zhí)行go build -gcflags=-m escape_d.go
#?command-line-arguments
./escape_d.go:7:9:?can?inline?Fibonacci.func1
./escape_d.go:17:13:?inlining?call?to?fmt.Printf
./escape_d.go:6:2:?moved?to?heap:?a
./escape_d.go:6:5:?moved?to?heap:?b
./escape_d.go:7:9:?func?literal?escapes?to?heap
./escape_d.go:17:34:?f()?escapes?to?heap
./escape_d.go:17:13:?[]interface?{}{...}?does?not?escape
<autogenerated>:1:?.this?does?not?escape
Fibonacci()函數(shù)中原本屬于局部變量的a和b,由于閉包的引用,不得不將二者放到堆上,從而產(chǎn)生逃逸
總結(jié)
-
逃逸分析在編譯階段完成
-
逃逸分析目的是決定內(nèi)分配地址是棧還是堆
-
棧上分配內(nèi)存比在堆中分配內(nèi)存有更高的效率
-
棧上分配的內(nèi)存不需要GC處理
-
堆上分配的內(nèi)存使用完畢會交給GC處理
通過逃逸分析,不逃逸的對象分配在棧上,當(dāng)函數(shù)返回時(shí)就回收了資源,不需gc標(biāo)記清除,從而減少gc的壓力
同時(shí),棧的分配比堆快,性能好(逃逸的局部變量會在堆上分配,而沒有發(fā)生逃逸的則有編譯器在棧上分配)
另外,還可以進(jìn)行同步消除: 如果定義的對象的方法上有同步鎖,但在運(yùn)行時(shí)卻只有一個線程在訪問,此時(shí)逃逸分析后的機(jī)器碼會去掉同步鎖運(yùn)行
全文參考自:
Go內(nèi)存管理之代碼的逃逸分析
Golang內(nèi)存分配逃逸分析[4]
推薦閱讀:
golang如何優(yōu)化編譯、逃逸分析、內(nèi)聯(lián)優(yōu)化
java逃逸技術(shù)分析[5]
譯文 Go 高性能系列教程之三:編譯器優(yōu)化[6]
參考資料
此文: http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
[2]Go則不一定(Java亦然): https://dashen.tech/2017/06/18/golang%E4%B8%ADnew-%E5%92%8Cmake-%E7%9A%84%E5%8C%BA%E5%88%AB/
[3]內(nèi)聯(lián)決策: https://dashen.tech/2021/05/22/Go%E4%B8%AD%E7%9A%84%E5%86%85%E8%81%94%E4%BC%98%E5%8C%96/
[4]Golang內(nèi)存分配逃逸分析: https://www.cnblogs.com/shijingxiang/articles/12200355.html
[5]java逃逸技術(shù)分析: https://blog.csdn.net/iechenyb/article/details/80925876
[6]譯文 Go 高性能系列教程之三:編譯器優(yōu)化: https://zhuanlan.zhihu.com/p/377397367
本文由 mdnice 多平臺發(fā)布