Go 語(yǔ)言機(jī)制之逃逸分析

參考:
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),意義。

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)如下所示。


Figure 1

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è)樣子。

Figure 2

如果看到的圖 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)這樣。


Figure 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)銷)地使用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容