什么網(wǎng)站系統(tǒng)做的最好的/磁力貓torrentkitty官網(wǎng)
“申請(qǐng)到棧內(nèi)存好處:函數(shù)返回直接釋放,不會(huì)引起垃圾回收,對(duì)性能沒(méi)有影響。
申請(qǐng)到堆上面的內(nèi)存才會(huì)引起垃圾回收。
func?F()?{
?a?:=?make([]int,?0,?20)
?b?:=?make([]int,?0,?20000)
?l?:=?20
?c?:=?make([]int,?0,?l)
}
“a和b代碼一樣,就是申請(qǐng)的空間不一樣大,但是它們兩個(gè)的命運(yùn)是截然相反的。a前面已經(jīng)介紹過(guò),會(huì)申請(qǐng)到棧上面,而b,由于申請(qǐng)的內(nèi)存較大,編譯器會(huì)把這種申請(qǐng)內(nèi)存較大的變量轉(zhuǎn)移到堆上面。即使是臨時(shí)變量,申請(qǐng)過(guò)大也會(huì)在堆上面申請(qǐng)。
而c,對(duì)我們而言其含義和a是一致的,但是編譯器對(duì)于這種不定長(zhǎng)度的申請(qǐng)方式,也會(huì)在堆上面申請(qǐng),即使申請(qǐng)的長(zhǎng)度很短。
堆(Heap)和棧(Stack)
參考 此文[1] <內(nèi)存模型:Heap>, <內(nèi)存模型:Stack>部分的內(nèi)容:
Heap:
堆的一個(gè)重要特點(diǎn)就是不會(huì)自動(dòng)消失,必須手動(dòng)釋放,或者由垃圾回收機(jī)制來(lái)回收。
Stack:
棧是由于函數(shù)運(yùn)行而臨時(shí)占用的內(nèi)存區(qū)域
執(zhí)行main函數(shù)時(shí),會(huì)為它在內(nèi)存里面建立一個(gè)幀(frame),所有main的內(nèi)部變量(比如a和b)都保存在這個(gè)幀里面。main函數(shù)執(zhí)行結(jié)束后,該幀就會(huì)被回收,釋放所有的內(nèi)部變量,不再占用空間。
一般來(lái)說(shuō),調(diào)用棧有多少層,就有多少幀。
所有的幀都存放在 Stack,由于幀是一層層疊加的,所以 Stack 被翻譯為棧。 (棧這個(gè)字的原始含義,就有柵欄的意思,所謂 棧道,棧橋,都是指比較簡(jiǎn)陋的用柵欄做的道路/橋梁)
即 在函數(shù)中申請(qǐng)一個(gè)新的對(duì)象:
如果分配 在棧中,則函數(shù)執(zhí)行結(jié)束可自動(dòng)將內(nèi)存回收;不會(huì)引起垃圾回收,對(duì)性能沒(méi)有影響。
如果分配在堆中,則函數(shù)執(zhí)行結(jié)束可交給GC(垃圾回收)處理;如果這個(gè)過(guò)程(特指垃圾回收不斷被觸發(fā))過(guò)于高頻就會(huì)導(dǎo)致 gc 壓力過(guò)大,程序性能出問(wèn)題。
C/C++中的new都是分配到堆上,Go則不一定(Java亦然)[2]
何為逃逸分析(Escape analysis)
在堆上分配的內(nèi)存,需要GC去回收,而在棧上分配,函數(shù)執(zhí)行完就銷毀,不存在垃圾回收的問(wèn)題. 所以應(yīng)盡可能將內(nèi)存分配在棧上.
但問(wèn)題是,對(duì)于一個(gè)函數(shù)或變量,并不能知道還有沒(méi)有其他地方在引用. 所謂的逃逸分析,就是為了確定這個(gè)事兒~
Go編譯器會(huì)跨越函數(shù)和包的邊界進(jìn)行全局的逃逸分析。它會(huì)檢查是否需要在堆上為一個(gè)變量分配內(nèi)存,還是說(shuō)可以在棧本身的內(nèi)存里對(duì)其進(jìn)行管理。
何時(shí)發(fā)生逃逸分析?
“Go編譯器決定變量應(yīng)該分配到什么地方時(shí)會(huì)進(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)確地說(shuō),你并不需要知道。Golang 中的變量只要被引用就一直會(huì)存活,存儲(chǔ)在堆上還是棧上由內(nèi)部實(shí)現(xiàn)決定而和具體的語(yǔ)法沒(méi)有關(guān)系。
但知道變量的存儲(chǔ)位置確實(shí)對(duì)程序的效率有幫助。如果可能,Golang 編譯器會(huì)將函數(shù)的局部變量分配到函數(shù)棧幀(stack frame)上。然而,如果編譯器不能確保變量在函數(shù) return 之后不再被引用,編譯器就會(huì)將變量分配到堆上。而且,如果一個(gè)局部變量非常大,那么它也應(yīng)該被分配到堆上而不是棧上。 當(dāng)前情況下,如果一個(gè)變量被取地址,那么它就有可能被分配到堆上。然而,還要對(duì)這些變量做逃逸分析,如果函數(shù) return 之后,變量不再被引用,則將其分配到棧上。
可以使用go命令的 -gcflags="-m"
選項(xiàng),來(lái)觀察逃逸分析的結(jié)果以及GC工具鏈的內(nèi)聯(lián)決策[3] ([內(nèi)聯(lián)是一種手動(dòng)或編譯器優(yōu)化,用于將簡(jiǎn)短函數(shù)的調(diào)用替換為函數(shù)體本身。這么做的原因是它可以消除函數(shù)調(diào)用本身的開(kāi)銷,也使得編譯器能更高效地執(zhí)行其他的優(yōu)化策略。我們可以顯式地在函數(shù)定義前面加一行//go:noinline
注釋讓編譯器不對(duì)函數(shù)進(jìn)行內(nèi)聯(lián))
實(shí)例
對(duì)于escape1.go代碼如下:
package?main
import?"fmt"
func?main()?{
?fmt.Println("Called?stackAnalysis",?stackAnalysis())
}
//go:noinline
func?stackAnalysis()?int?{
?data?:=?100
?return?data
}
通過(guò) go build -gcflags "-m -l" escape1.go
go build -gcflags=-m escape1.go
來(lái)查看和分析逃逸分析:
./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ù)棧之間共享(這個(gè)例子就是在main和fmt.Println之間在棧上共享)
-
第6行第13個(gè)字符處的字符串標(biāo)量"Called stackAnalysis"逃逸到堆上
-
第6行51個(gè)字符處的函數(shù)調(diào)用stackAnalysis()逃逸到了堆上
對(duì)于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ù)中會(huì)使用該指針變量. 因?yàn)槭窃趆eapAnalysis函數(shù)外部訪問(wèn),所以data變量必須被移動(dòng)到堆上
主函數(shù)main會(huì)從堆中訪問(wèn)該data變量
(可見(jiàn)指針雖能夠減少變量在函數(shù)間傳遞時(shí)的數(shù)據(jù)值拷貝,但不該所有類型數(shù)據(jù)都返回其指針.如果分配到堆上的共享變量太多會(huì)增加了GC的壓力)
逃逸類型
1. 指針逃逸:
對(duì)于 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)的局部變量, 其值通過(guò)函數(shù)返回值返回. 但s 本身為指針類型. 所以其指向的內(nèi)存地址不會(huì)是棧而是堆. **
這是一種典型的變量逃逸案例
2. ??臻g不足而導(dǎo)致的逃逸(空間開(kāi)辟過(guò)大):
對(duì)于 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í)并沒(méi)有發(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)前對(duì)象,或無(wú)法判斷當(dāng)前切片長(zhǎng)度時(shí),會(huì)將對(duì)象分配到堆中
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
沒(méi)有逃逸.
而改成
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ā)生了逃逸
這是為何? 參見(jiàn)下文!
3. 動(dòng)態(tài)類型逃逸(不確定長(zhǎng)度大小):
當(dāng)函數(shù)參數(shù)為**interface{}**類型, 如最常用的fmt.Println(a …interface{})
, 編譯期間很難確定其參數(shù)的具體類型,也會(huì)產(chǎn)生逃逸
對(duì)于 escape_c1.go:
package?main
import?"fmt"
func?main()?{
?s?:=?"s會(huì)發(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
對(duì)于 escape_c1.go:
package?main
func?main()?{
?InitSlice2()
}
func?InitSlice2()?{
?a?:=?make([]int,?0,?20)????//?棧?空間小
?b?:=?make([]int,?0,?20000)?//?堆?空間過(guò)大?逃逸
?l?:=?20
?c?:=?make([]int,?0,?l)?//?堆?動(dòng)態(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. 閉包引用對(duì)象逃逸:
對(duì)于如下斐波那契數(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)存使用完畢會(huì)交給GC處理
通過(guò)逃逸分析,不逃逸的對(duì)象分配在棧上,當(dāng)函數(shù)返回時(shí)就回收了資源,不需gc標(biāo)記清除,從而減少gc的壓力
同時(shí),棧的分配比堆快,性能好(逃逸的局部變量會(huì)在堆上分配,而沒(méi)有發(fā)生逃逸的則有編譯器在棧上分配)
另外,還可以進(jìn)行同步消除: 如果定義的對(duì)象的方法上有同步鎖,但在運(yùn)行時(shí)卻只有一個(gè)線程在訪問(wèn),此時(shí)逃逸分析后的機(jī)器碼會(huì)去掉同步鎖運(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 多平臺(tái)發(fā)布