閱讀前請悉知:本文是一篇翻譯文章抚吠,出于對原文的喜愛與敬畏楷力,所以需要強(qiáng)調(diào):如果讀者英文閱讀能力好孵户,請直接移步文末原文鏈接延届;如果對這篇翻譯所述知識感興趣方庭,也請一定要再看下英文原文械念,加深理解龄减。翻譯中為了表達(dá)的需要希停,加入了自己的一些理解,不過因?yàn)橹R有限亚隙,翻譯過程難免紕漏阿弃,如有問題羞延,歡迎留言指正伴箩。
前言
在這個(gè)由四部分組成的系列的第一篇文章中赛蔫,我使用了一個(gè)例子來介紹指針機(jī)制的基礎(chǔ)知識呵恢,在這個(gè)例子中渗钉,一個(gè)值被共享到goroutine
的棧中。我沒有向你們展示的是當(dāng)你在棧上共享一個(gè)值時(shí)會(huì)發(fā)生什么声离。要理解這一點(diǎn)术徊,你需要了解另一個(gè)內(nèi)存區(qū)域:堆赠涮。有了這些知識笋除,你就可以開始學(xué)習(xí)逃逸分析了垃它。
逃逸分析是編譯器用來確定由程序創(chuàng)建的值所處位置的過程国拇。具體來說贝奇,編譯器執(zhí)行靜態(tài)代碼分析掉瞳,以確定是否可以將值放在構(gòu)造函數(shù)的棧(幀)上,或者該值是否必須“逃逸”到堆上霎褐。在Go中冻璃,沒有關(guān)鍵字或函數(shù)可以用于在此決策中指導(dǎo)編譯器省艳。只有通過你寫的代碼來分析這一點(diǎn)跋炕。
堆
堆是除棧之外的第二個(gè)內(nèi)存區(qū)域辐烂,用于存儲值纠修。堆不像棧那樣是自清理的厂僧,因此使用這個(gè)內(nèi)存的成本更大颜屠。首先汽纤,成本與垃圾收集器(GC)有關(guān)蕴坪,垃圾收集器必須參與進(jìn)來以保持該區(qū)域的清潔背传。當(dāng)GC運(yùn)行時(shí),它將使用25%的可用CPU資源痴脾。此外赞赖,它可能會(huì)產(chǎn)生微秒級的“stop the world”延遲冤灾。擁有GC的好處是你不需要擔(dān)心內(nèi)存的管理問題韵吨,因?yàn)閮?nèi)存管理是相當(dāng)復(fù)雜归粉、也容易出錯(cuò)的。
堆上的值構(gòu)成Go中的內(nèi)存分配届榄。這些分配對GC造成壓力痒蓬,因?yàn)槎阎胁辉俦恢羔樢玫拿總€(gè)值都需要?jiǎng)h除滴劲。需要檢查和刪除的值越多班挖,GC每次運(yùn)行時(shí)必須執(zhí)行的工作就越多萧芙。因此双揪,GC算法一直在努力在堆的大小分配和運(yùn)行速度之間尋求平衡。
共享?xiàng)?/h1>
在Go中运吓,不允許goroutine
擁有指向另一個(gè)goroutine
棧上的內(nèi)存的指針拘哨。這是因?yàn)楫?dāng)棧必須增長或收縮時(shí),goroutine
的棧內(nèi)存可能被一個(gè)新的內(nèi)存塊替換瓮床。如果運(yùn)行時(shí)必須跟蹤指向其他goroutine
棧的指針隘庄,那么管理起來就太困難了峭沦,而在這些棧
上更新指針的“stop the world”延遲將會(huì)非常困難逃糟。
下面是一個(gè)由于增長而多次被替換的棧示例绰咽。查看第2行和第6行的輸出琐谤。你將在main
的棧(幀)中看到字符串值的地址更改了兩次玩敏。(字符串s
的內(nèi)存地址本來應(yīng)該是在main的幀內(nèi)的,為何會(huì)發(fā)生這種變化呢旺聚?沒搞懂)
// Sample program to show how stacks grow/change.
package main
// Number of elements to grow each stack frame.
// Run with 10 and then with 1024
const size = 1024
// main is the entry point for the application.
func main() {
s := "HELLO"
stackCopy(&s, 0, [size]int{})
}
// stackCopy recursively runs increasing the size
// of the stack.
func stackCopy(s *string, c int, a [size]int) {
println(c, s, *s)
c++
if c == 10 {
return
}
stackCopy(s, c, a)
}
輸出
0 0x1044dfa0 HELLO
1 0x1044dfa0 HELLO
2 0x10455fa0 HELLO
3 0x10455fa0 HELLO
4 0x10455fa0 HELLO
5 0x10455fa0 HELLO
6 0x10465fa0 HELLO
7 0x10465fa0 HELLO
8 0x10465fa0 HELLO
9 0x10465fa0 HELLO
逃逸機(jī)制
在函數(shù)的棧(幀)之外共享一個(gè)值時(shí)唧躲,它將被放置(或分配)在堆上碱璃。逃逸分析算法的工作是找到這些情況嵌器,并在程序中保持一定的完整性爽航。完整性在于確保對任何值的訪問總是準(zhǔn)確庇谆、一致和高效的。
Listing 1
01 package main
02
03 type user struct {
04 name string
05 email string
06 }
07
08 func main() {
09 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
指令执解,以防止編譯器直接內(nèi)聯(lián)這些函數(shù)的代碼衰腌。內(nèi)聯(lián)將刪除函數(shù)調(diào)用并使這個(gè)示例復(fù)雜化右蕊。我將在下一篇文章中介紹內(nèi)聯(lián)的副作用吮螺。
在清單1中鸠补,你將看到一個(gè)具有兩個(gè)不同函數(shù)的程序紫岩,它們創(chuàng)建用戶值并將值返回給調(diào)用者泉蝌。createUserV1
在返回時(shí)使用了值語義勋陪。
Listing 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 }
我說過函數(shù)在返回時(shí)使用值語義诅愚,因?yàn)檫@個(gè)函數(shù)創(chuàng)建的用戶值正在被復(fù)制并傳遞給調(diào)用棧呻粹。這意味著調(diào)用函數(shù)正在接收值本身的副本等浊。
你可以看到在第17行到第20行執(zhí)行了用戶值的構(gòu)造筹燕。然后在第23行,用戶值的副本被傳遞到調(diào)用棧并返回給調(diào)用者大渤。函數(shù)返回后泵三,棧是這樣的烫幕。
Figure 1
在圖1中可以看到较曼,在調(diào)用createUserV1
之后捷犹,兩個(gè)幀中都存在一個(gè)用戶值冕末。在函數(shù)的createUserV2
中栓霜,在返回時(shí)使用指針語義销凑。
Listing 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 }
我說過斗幼,函數(shù)在返回時(shí)使用指針語義蜕窿,因?yàn)檫@個(gè)函數(shù)創(chuàng)建的用戶值在調(diào)用棧中被共享桐经。這意味著調(diào)用函數(shù)正在接收該值的地址副本阴挣。
你可以看到畔咧,在第28到31行中使用了相同的struct文字構(gòu)造用戶值梅桩,但是在第34行中返回的值不同宿百。不是將用戶值的副本傳遞回調(diào)用棧,而是傳遞用戶值的地址副本薇组÷烧停基于此炭菌,你可能認(rèn)為調(diào)用之后棧是這樣的。
Figure 2
如果你在圖2中看到的真的發(fā)生了逛漫,那么你就會(huì)遇到完整性問題黑低。指針向下指向不再有效的調(diào)用棧。在main的下一個(gè)函數(shù)調(diào)用中酌毡,所指向的內(nèi)存將被重新構(gòu)造并重新初始化克握。
這就是逃逸分析開始維護(hù)完整性的地方。在這種情況下枷踏,編譯器將確定在createUserV2
的棧楨內(nèi)構(gòu)造用戶值是不安全的菩暗,因此它將在堆上構(gòu)造值。這將由第28行初始化完成旭蠕。
可讀性
正如你在上一篇文章中學(xué)到的停团,在所屬幀內(nèi),函數(shù)可以通過指針直接訪問楨內(nèi)的內(nèi)存,但是訪問幀外的內(nèi)存需要間接訪問辆琅。這意味著對轉(zhuǎn)義到堆的值的訪問也必須通過指針間接完成。
記住createUserV2
的代碼是什么樣子的。
Listing 4
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 }
語法隱藏了代碼中真正發(fā)生的事情。第28行聲明的變量u
表示user
類型的值。Go中的構(gòu)造不會(huì)告訴你一個(gè)值在內(nèi)存中的位置,所以直到第34行上的return
語句,你才知道這個(gè)值需要逃逸。這意味著,即使u
表示的是user
類型的值,訪問這個(gè)user
值也必須通過封面下面的指針進(jìn)行吓著。
你可以在函數(shù)調(diào)用之后將內(nèi)存布局形象化纺裁。
Figure 3
createUserV2
的棧(幀)上的u
變量表示堆上的值丧鸯,而不是棧上的值剿干。這意味著使用u
訪問值,需要指針訪問腐芍,而不是語法建議的直接訪問。你可能會(huì)想,既然訪問它所代表的值需要使用指針,那么為什么不讓u
成為指針呢?
Listing 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 }
如果你這樣做员舵,會(huì)犧牲代碼的?可讀性。暫時(shí)離開整個(gè)函數(shù)溶弟,只關(guān)注返回值。
Listing 6
34 return u
35 }
這還告訴你什么?它說的是一個(gè)u
的副本被傳遞到調(diào)用棧上。然而纳鼎,當(dāng)你使用&
操作符時(shí),返回告訴你什么?
Listing 7
34 return &u
35 }
多虧了&
運(yùn)算符,返回現(xiàn)在告訴因?yàn)?code>u需要共享到調(diào)用棧中,由此逃逸到堆中宫补。記住荞雏,指針是用于共享的俺驶,并在讀取代碼時(shí)替換“共享”一詞的&
操作符。這在可讀性方面非常強(qiáng)大塘幅,這是你不想失去的钝尸。
下面是另一個(gè)例子娇斩,使用指針語義構(gòu)造值會(huì)損害可讀性。
Listing 8
01 var u *user
02 err := json.Unmarshal([]byte(r), &u)
03 return u, err
你必須與json
共享指針變量。在第02行調(diào)用Unmarshal
嗽元,讓這段代碼工作辞做。json.Unmarshal
調(diào)用將創(chuàng)建用戶值并將其地址分配給指針變量五垮。
這段代碼說了什么:
01:創(chuàng)建一個(gè)類型為user
的指針變量惶傻。
02:與json.Unmarshal
函數(shù)共享u
。
03:給調(diào)用方返回u
的副本狱从。
user
的值是否由json.Unmarshal
函數(shù)創(chuàng)建并與調(diào)用者共享尚不清楚驼卖。
在構(gòu)造過程中使用值語義時(shí)可讀性如何變化?
Listing 9
01 var u user
02 err := json.Unmarshal([]byte(r), &u)
03 return &u, err
這段代碼說了什么:
01:創(chuàng)建一個(gè)類型為user
的指針變量。
02:與json.Unmarshal
函數(shù)共享u
桥胞。
03:與調(diào)用方共享u
。
一切都很清楚奔则。第02行將調(diào)用棧中的user
值共享給json.Unmarshal
函數(shù)以及第03行將user
值從調(diào)用棧上共享給調(diào)用者及老。此共享將導(dǎo)致用戶值轉(zhuǎn)義象泵。
在構(gòu)造值時(shí)使用值語義忽孽,并利用&
運(yùn)算符的可讀性來明確值是如何被共享的。
編譯器報(bào)告
要查看編譯器正在做出的決策召廷,你可以要求編譯器提供一個(gè)報(bào)告。你所需要做的就是在go build
調(diào)用中使用帶有-m
選項(xiàng)的-gcflags
開關(guān)账胧。
實(shí)際上有4個(gè)級別的-m
可以使用竞慢,但是超過2個(gè)級別的信息就會(huì)讓人不知所措。我將使用-m
的兩個(gè)級別治泥。
Listing 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)告逃逸情況筹煮。編譯器在說什么?首先再次查看createUserV1
和createUserV2
函數(shù)以供參考。
Listing 13
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 }
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)告中的這一行開始居夹。
Listing 14
./main.go:22: createUserV1 &u does not escape
這就是說败潦,在createUserV1
函數(shù)中對println
的函數(shù)調(diào)用不會(huì)導(dǎo)致用戶值轉(zhuǎn)義到堆中本冲。必須檢查它,因?yàn)樗谂cprintln
函數(shù)共享劫扒。
接下來看看報(bào)告中的這些行檬洞。
Listing 15
./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
這些行表示,與u
變量關(guān)聯(lián)的user
值(它是命名類型user
沟饥,在第31行分配)因?yàn)樵诘?4行返回而轉(zhuǎn)義疮胖。最后一行和前面一樣,第33行上的println
調(diào)用不會(huì)導(dǎo)致用戶值轉(zhuǎn)義闷板。
閱讀這些報(bào)告可能會(huì)讓人感到困惑澎灸,并可能會(huì)根據(jù)所涉及的變量類型是基于命名類型還是基于文字類型而略有變化。
將u
更改為文字類型*user
遮晚,而不是之前的命名類型user
性昭。
Listing 16
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)告
Listing 17
./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)告說县遣,由于在第34行返回糜颠,由u變量引用的用戶值正在轉(zhuǎn)義,該變量是文本類型*user萧求,在第28行賦值其兴。
結(jié)論
一個(gè)值的構(gòu)造并不決定它的位置。只有如何共享一個(gè)值才能決定編譯器將如何處理該值夸政。任何時(shí)候你在調(diào)用棧上共享一個(gè)值元旬,它都會(huì)被轉(zhuǎn)義。在下一篇文章中守问,你將探討其他原因來解釋值的轉(zhuǎn)義匀归。
這些帖子試圖引導(dǎo)你為任何給定類型選擇值或指針語義的指導(dǎo)原則。每種語義都有其優(yōu)點(diǎn)和代價(jià)耗帕。值語義將值保存在棧上穆端,從而減少對GC的壓力。但是仿便,任何給定值都有不同的副本体啰,必須存儲、跟蹤和維護(hù)嗽仪。指針語義將值放在堆上荒勇,這會(huì)對GC造成壓力。但是钦幔,它們是高效的枕屉,因?yàn)橹挥幸粋€(gè)值需要存儲、跟蹤和維護(hù)鲤氢。關(guān)鍵是正確搀擂、一致和平衡地使用每個(gè)語義。
版權(quán)聲明:
任何個(gè)人或機(jī)構(gòu)如需轉(zhuǎn)載本文卷玉,無須再獲得作者書面授權(quán)哨颂,但是轉(zhuǎn)載者必須保留作者署名,并注明出處相种。
作者保留對本文的修改權(quán)威恼。他人未經(jīng)作者許可,不得擅自修改寝并,破壞作品的完整性箫措。
作者保留對本文的其他各項(xiàng)著作權(quán)權(quán)利。