參考:
https://www.cnblogs.com/shijingxiang/articles/12200355.html
原文:
https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html
翻譯:
https://blog.csdn.net/weixin_38975685/article/details/79788254
關(guān)鍵點(diǎn)
變量是分配到棧,還是堆,
是誰(shuí)來(lái)決定?---> 只能通過(guò)自己的代碼方式變量的類型,不能 決定是
分配到棧,還是分配到堆的,即使是指針也不一定分配到堆&符號(hào),表示 變量的共享,或者說(shuō)內(nèi)存的共享(很重要,記住,其他知識(shí)點(diǎn),作為了解就可以了)-
&使用這個(gè),增加 代碼的可讀性,如- ruturn &ptr, 很明顯,這個(gè)返還的是
地址 - var ptr *int , return ptr, 有的時(shí)候,從字面看不出來(lái),意義。
- ruturn &ptr, 很明顯,這個(gè)返還的是
1、介紹
在四部分系列的第一部分,我用一個(gè)將值共享給 goroutine 棧的例子介紹了指針結(jié)構(gòu)的基礎(chǔ)。而我沒(méi)有說(shuō)的是值存在棧之上的情況。
為了理解這個(gè),你需要學(xué)習(xí)值存儲(chǔ)的另外一個(gè)位置:堆。有這個(gè)基礎(chǔ),就可以開(kāi)始學(xué)習(xí)逃逸分析。
逃逸分析是編譯器用來(lái)決定你的程序中值的位置的過(guò)程。
特別地,編譯器執(zhí)行靜態(tài)代碼分析,以確定一個(gè)構(gòu)造體的實(shí)例化值是否會(huì)逃逸到堆。
在 Go 語(yǔ)言中,你沒(méi)有可用的關(guān)鍵字或者函數(shù),能夠直接讓編譯器做這個(gè)決定。
只能夠通過(guò)你寫代碼的方式來(lái)作出這個(gè)決定。
2、堆 Heaps
堆是內(nèi)存的第二區(qū)域,除了棧之外,用來(lái)存儲(chǔ)值的地方。
堆無(wú)法像棧一樣能自清理,所以使用這部分內(nèi)存會(huì)造成很大的開(kāi)銷(相比于使用棧)。
重要的是,開(kāi)銷跟 GC(垃圾收集),即被牽扯進(jìn)來(lái)保證這部分區(qū)域干凈的程序,有很大的關(guān)系。
當(dāng)垃圾收集程序運(yùn)行時(shí),它會(huì)占用你的可用 CPU 容量的 25%。更有甚者,它會(huì)造成微秒級(jí)的 “stop the world” 的延時(shí)。
擁有 GC 的好處是你可以不再關(guān)注堆內(nèi)存的管理,這部分很復(fù)雜,是歷史上容易出錯(cuò)的地方。
在 Go 中,會(huì)將一部分值分配到堆上。這些分配給 GC 帶來(lái)了壓力,因?yàn)槎焉蠜](méi)有被指針?biāo)饕闹刀夹枰粍h除。
越多需要被檢查和刪除的值,會(huì)給每次運(yùn)行 GC 時(shí)帶來(lái)越多的工作。
所以,分配算法不斷地工作,以平衡堆的大小和它運(yùn)行的速度。
3、共享?xiàng)?Sharing Stacks
在 Go 語(yǔ)言中,不允許 goroutine 中的指針指向另外一個(gè) goroutine 的棧。這是因?yàn)楫?dāng)棧增長(zhǎng)或者收縮時(shí),goroutine 中的棧內(nèi)存會(huì)被一塊新的內(nèi)存替換。
如果運(yùn)行時(shí)需要追蹤指針指向其他的 goroutine 的棧,就會(huì)造成非常多需要管理的內(nèi)存,以至于更新指向那些棧的指針將使 “stop the world” 問(wèn)題更嚴(yán)重。
這里有一個(gè)棧被替換好幾次的例子。看輸出的第 2 和第 6 行。你會(huì)看到 main 函數(shù)中的棧的字符串地址值改變了兩次。
https://play.golang.org/p/pxn5u4EBSI
4、逃逸機(jī)制 Escape Mechanics
任何時(shí)候,一個(gè)值被分享到函數(shù)棧幀范圍之外,它都會(huì)在堆上被重新分配。
這是逃逸分析算法發(fā)現(xiàn)這些情況和管控這一層的工作。
(內(nèi)存的)完整性在于確保對(duì)任何值的訪問(wèn)始終是準(zhǔn)確、一致和高效的。
通過(guò)查看這個(gè)語(yǔ)言機(jī)制了解逃逸分析。
https://play.golang.org/p/Y_VZxYteKO
4.1、表1
1: package main
2:
3: type user struct {
4: name string
5: email string
6: }
7:
8: func main() {
9: u1 := createUserV1()
10: u2 := createUserV2()
11:
12: println("u1", &u1, "u2", &u2)
13: }
14:
15: //go:noinline
16: func createUserV1() user {
17: u := user{
18: name: "Bill",
19: email: "bill@ardanlabs.com",
20: }
21:
22: println("V1", &u)
23: return u
24: }
25:
26: //go:noinline
27: func createUserV2() *user {
28: u := user{
29: name: "Bill",
30: email: "bill@ardanlabs.com",
31: }
32:
33: println("V2", &u)
34: return &u
35: }
我使用 go:noinline 指令,阻止在 main 函數(shù)中,編譯器使用內(nèi)聯(lián)代碼替代函數(shù)調(diào)用。內(nèi)聯(lián)(優(yōu)化)會(huì)使函數(shù)調(diào)用消失,并使例子復(fù)雜化。
我將在下一篇博文介紹內(nèi)聯(lián)造成的副作用。
在表 1 中,你可以看到創(chuàng)建 user 值,并返回給調(diào)用者的兩個(gè)不同的函數(shù)。在函數(shù)版本 1 中,返回值。
4.2、表 2
16: func createUserV1() user {
17: u := user{
18: name: "Bill",
19: email: "bill@ardanlabs.com",
20: }
21:
22: println("V1", &u)
23: return u
24: }
我說(shuō)這個(gè)函數(shù)返回的是值是因?yàn)檫@個(gè)被函數(shù)創(chuàng)建的 user 值被拷貝并傳遞到調(diào)用棧上。
這意味著調(diào)用函數(shù)接收到的是這個(gè)值的拷貝。
你可以看下第 17 行到 20 行 user 值被構(gòu)造的過(guò)程。然后在第 23 行,user 值的副本被傳遞到調(diào)用棧并返回給調(diào)用者。函數(shù)返回后,??雌饋?lái)如下所示。

4.3、表 3
27: func createUserV2() *user {
28: u := user{
29: name: "Bill",
30: email: "bill@ardanlabs.com",
31: }
32:
33: println("V2", &u)
34: return &u
35: }
我說(shuō)這個(gè)函數(shù)返回的是指針是因?yàn)檫@個(gè)被函數(shù)創(chuàng)建的 user 值通過(guò)調(diào)用棧被共享了。
這意味著調(diào)用函數(shù)接收到一個(gè)值的地址拷貝。
你可以看到在第 28 行到 31 行使用相同的字段值來(lái)構(gòu)造 user 值,但在第 34 行返回時(shí)卻是不同的。
不是將 user 值的副本傳遞到調(diào)用棧,而是將 user 值的地址傳遞到調(diào)用棧?;诖?,你也許會(huì)認(rèn)為棧在調(diào)用之后是這個(gè)樣子。

如果看到的圖 2 真的發(fā)生的話,你將遇到一個(gè)問(wèn)題。指針指向了棧下的無(wú)效地址空間。
當(dāng) main 函數(shù)調(diào)用下一個(gè)函數(shù),指向的內(nèi)存將重新映射并將被重新初始化。
這就是逃逸分析將開(kāi)始保持完整性的地方。在這種情況下,編譯器將檢查到,在 createUserV2 的(函數(shù))棧中構(gòu)造 user 值是不安全的,因此,替代地,會(huì)在堆中構(gòu)造(相應(yīng)的)值。
這(個(gè)分析并處理的過(guò)程)將在第 28 行構(gòu)造時(shí)立即發(fā)生。
5、可讀性
在上一篇博文中,我們知道一個(gè)函數(shù)只能直接訪問(wèn)它的(函數(shù)棧)空間,或者通過(guò)(函數(shù)棧空間內(nèi)的)指針,通過(guò)跳轉(zhuǎn)訪問(wèn)(函數(shù)??臻g外的)外部?jī)?nèi)存。這意味著訪問(wèn)逃逸到堆上的值也需要通過(guò)指針跳轉(zhuǎn)。
記住 createUserV2 的代碼的樣子
27: func createUserV2() *user {
28: u := user{
29: name: "Bill",
30: email: "bill@ardanlabs.com",
31: }
32:
33: println("V2", &u)
34: return &u
35: }
語(yǔ)法隱藏了代碼中真正發(fā)生的事情。
第 28 行聲明的變量 u 代表一個(gè) user 類型的值。
Go 代碼中的類型構(gòu)造不會(huì)告訴你值在內(nèi)存中的位置。
所以直到第 34 行返回類型時(shí),你才知道值需要逃逸(處理)。
這意味著,雖然 u 代表類型 user 的一個(gè)值,但對(duì)該值的訪問(wèn)必須通過(guò)指針進(jìn)行。
你可以在函數(shù)調(diào)用之后,看到堆棧就像(圖 3)這樣。

在 createUserV2 函數(shù)棧中,變量 u 代表的值存在于堆中,而不是棧。這意味著用 u 訪問(wèn)值時(shí),使用指針訪問(wèn)而不是直接訪問(wèn)。
你可能想,為什么不讓 u 成為指針,畢竟訪問(wèn)它代表的值需要使用指針?
5.1、表 5
27: func createUserV2() *user {
28: u := &user{
29: name: "Bill",
30: email: "bill@ardanlabs.com",
31: }
32:
33: println("V2", u)
34: return u
35: }
如果你這樣做,將使你的代碼缺乏重要的可讀性。(讓我們)離開(kāi)整個(gè)函數(shù)一秒,只關(guān)注 return。
5.2、表 6
34: return u
35: }
這個(gè) return 告訴你什么了呢?
它說(shuō)明了返回 u 值的副本給調(diào)用棧。然而,當(dāng)你使用 & 操作符,return 又告訴你什么了呢?
5.3、表 7
34: return &u
35: }
多虧了 & 操作符,return 告訴你 u 被分享給調(diào)用者,因此,已經(jīng)逃逸到堆中。
記住,當(dāng)你讀代碼的時(shí)候,指針是為了共享,& 操作符對(duì)應(yīng)單詞 “sharing”。
這在提高可讀性的時(shí)候非常有用,這(也)是你不想失去的部分。
5.4、表 8
01 var u *user
02 err := json.Unmarshal([]byte(r), &u)
03 return u, err
為了讓其可以工作,你一定要通過(guò)共享指針變量(的方式)給(函數(shù)) json.Unmarshal。json.Unmarshal 調(diào)用時(shí)會(huì)創(chuàng)建 ***user 值并將其地址賦值給指針變量。https://play.golang.org/p/koI8EjpeIx
代碼解釋:
01:創(chuàng)建一個(gè)類型為 user,值為空的指針。
02:跟函數(shù) json.Unmarshal 函數(shù)共享指針。
03:返回 u 的副本給調(diào)用者。
這里并不是很好理解,user值被 json.Unmarshal 函數(shù)創(chuàng)建,并被共享給調(diào)用者。
如何在構(gòu)造過(guò)程中使用語(yǔ)法語(yǔ)義來(lái)改變可讀性?
5.5、表 9(比較重要,涉及到以后,是聲明變量,還是指針變量)
01 var u user
02 err := json.Unmarshal([]byte(r), &u)
03 return &u, err
代碼解釋:
01:創(chuàng)建一個(gè)類型為 user,值為空的變量。
02:跟函數(shù) json.Unmarshal 函數(shù)共享 u。
03:跟調(diào)用者共享 u。
這里非常好理解。第 02 行共享 user 值到調(diào)用棧中的 json.Unmarshal,在第 03 行 user 值共享給調(diào)用者。這個(gè)共享過(guò)程將會(huì)導(dǎo)致 user 值逃逸。
在構(gòu)建一個(gè)值時(shí),使用值語(yǔ)義,并利用 & 操作符的可讀性來(lái)明確值是如何被共享的。
6、編譯器報(bào)告 Compiler Reporting
想查看編譯器(關(guān)于逃逸分析)的決定,你可以讓編譯器提供一份報(bào)告。
你只需要在調(diào)用 go build 的時(shí)候,打開(kāi) -gcflags 開(kāi)關(guān),并帶上 -m 選項(xiàng)。
實(shí)際上總共可以使用 4 個(gè) -m,(但)超過(guò) 2 個(gè)級(jí)別的信息就已經(jīng)太多了。我將使用 2 個(gè) -m 的級(jí)別。
6.1、表10
$ go build -gcflags "-m -m"
./main.go:16: cannot inline createUserV1: marked go:noinline
./main.go:27: cannot inline createUserV2: marked go:noinline
./main.go:8: cannot inline main: non-leaf function
./main.go:22: createUserV1 &u does not escape
./main.go:34: &u escapes to heap
./main.go:34: from ~r0 (return) at ./main.go:34
./main.go:31: moved to heap: u
./main.go:33: createUserV2 &u does not escape
./main.go:12: main &u1 does not escape
./main.go:12: main &u2 does not escape
你可以看到編譯器報(bào)告是否需要逃逸處理的決定。編譯器都說(shuō)了什么呢?
請(qǐng)?jiān)倏匆幌乱玫?createUserV1 和 createUserV2 函數(shù)。
6.2、表 11
16: func createUserV1() user {
17: u := user{
18: name: "Bill",
19: email: "bill@ardanlabs.com",
20: }
21:
22: println("V1", &u)
23: return u
24: }
25:
26: //go:noinline
27: func createUserV2() *user {
28: u := user{
29: name: "Bill",
30: email: "bill@ardanlabs.com",
31: }
32:
33: println("V2", &u)
34: return &u
35: }
從報(bào)告中的這一行開(kāi)始。
6.3、表 12
./main.go:22: createUserV1 &u does not escape
這是說(shuō)在函數(shù) createUserV1 調(diào)用 println 不會(huì)造成 user 值逃逸到堆。
這是必須檢查的,因?yàn)樗鼘?huì)跟函數(shù) println 共享(u)。
接下來(lái)看報(bào)告中的這幾行。
6.4、表 13
./main.go:34: &u escapes to heap
./main.go:34: from ~r0 (return) at ./main.go:34
./main.go:31: moved to heap: u
./main.go:33: createUserV2 &u does not escape
這幾行是說(shuō),類型為 user,并在第 31 行被賦值的 u 的值,因?yàn)榈?34 行的 return 逃逸。最后一行是說(shuō),跟之前一樣,在 33 行調(diào)用 println 不會(huì)造成 user 值逃逸。
閱讀這些報(bào)告可能讓人感到困惑,(編譯器)會(huì)根據(jù)所討論的變量的類型是基于值類型還是指針類型而略有變化。
將 u 改為指針類型的 *user,而不是之前的命名類型 user。
6.5、表 14
27: func createUserV2() *user {
28: u := &user{
29: name: "Bill",
30: email: "bill@ardanlabs.com",
31: }
32:
33: println("V2", u)
34: return u
35: }
再次生成報(bào)告。
6.6、表 15
./main.go:30: &user literal escapes to heap
./main.go:30: from u (assigned) at ./main.go:28
./main.go:30: from ~r0 (return) at ./main.go:34
現(xiàn)在報(bào)告說(shuō)在 28 行賦值的指針類型 *user,u 引用的 user 值,因?yàn)?34 行的 return 逃逸。
7、結(jié)論
值在構(gòu)建時(shí)并不能決定它將存在于哪里。只有當(dāng)一個(gè)值被共享,編譯器才能決定如何處理這個(gè)值。當(dāng)你在調(diào)用時(shí),共享了棧上的一個(gè)值時(shí),它就會(huì)逃逸。在下一篇中你將探索一個(gè)值逃逸的其他原因。
這些文章試圖引導(dǎo)你選擇給定類型的值或指針的指導(dǎo)原則。每種方式都有(對(duì)應(yīng)的)好處和(額外的)開(kāi)銷。保持在棧上的值,減少了 GC 的壓力。但是需要存儲(chǔ),跟蹤和維護(hù)不同的副本。將值放在堆上的指針,會(huì)增加 GC 的壓力。然而,也有它的好處,只有一個(gè)值需要存儲(chǔ),跟蹤和維護(hù)。(其實(shí),)最關(guān)鍵的是如何保持正確地、一致地以及均衡(開(kāi)銷)地使用。