Go語言的逃逸分析機(jī)制

閱讀前請悉知:本文是一篇翻譯文章抚吠,出于對原文的喜愛與敬畏楷力,所以需要強(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

image.png

在圖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

image.png

如果你在圖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

image.png

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)告逃逸情況筹煮。編譯器在說什么?首先再次查看createUserV1createUserV2函數(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)利。

原文閱讀:
Language Mechanics On Escape Analysis

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衬潦,一起剝皮案震驚了整個(gè)濱河市斤蔓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镀岛,老刑警劉巖弦牡,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異漂羊,居然都是意外死亡驾锰,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門走越,熙熙樓的掌柜王于貴愁眉苦臉地迎上來椭豫,“玉大人,你說我怎么就攤上這事旨指∧砻酰” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵淤毛,是天一觀的道長今缚。 經(jīng)常有香客問我,道長低淡,這世上最難降的妖魔是什么姓言? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮蔗蹋,結(jié)果婚禮上何荚,老公的妹妹穿的比我還像新娘。我一直安慰自己猪杭,他們只是感情好餐塘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著皂吮,像睡著了一般戒傻。 火紅的嫁衣襯著肌膚如雪税手。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天需纳,我揣著相機(jī)與錄音芦倒,去河邊找鬼。 笑死不翩,一個(gè)胖子當(dāng)著我的面吹牛兵扬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播口蝠,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼器钟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了妙蔗?” 一聲冷哼從身側(cè)響起傲霸,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎灭必,沒想到半個(gè)月后狞谱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡禁漓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年跟衅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片播歼。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伶跷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秘狞,到底是詐尸還是另有隱情叭莫,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布烁试,位于F島的核電站雇初,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏减响。R本人自食惡果不足惜靖诗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望支示。 院中可真熱鬧刊橘,春花似錦、人聲如沸颂鸿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至败晴,卻和暖如春浓冒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背位衩。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工裆蒸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留熔萧,地道東北人糖驴。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像佛致,于是被迫代替她去往敵國和親贮缕。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 閱讀前請悉知:本文是一篇翻譯文章俺榆,出于對原文的喜愛與敬畏感昼,所以需要強(qiáng)調(diào):如果讀者英文閱讀能力好,請直接移步文末原文...
    wu_sphinx閱讀 960評論 0 0
  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,783評論 0 38
  • 一個(gè)項(xiàng)目的開始罐脊,首先要確定要做的項(xiàng)目是什么定嗓,還要經(jīng)過市場調(diào)研,看這個(gè)項(xiàng)目是否有做的必要萍桌,以及自己做的項(xiàng)目的優(yōu)勢是什...
    eff7af6c2f06閱讀 149評論 0 2
  • 誤會(huì)我談戀愛了 從同學(xué)她媽那兒聽來的 呵呵 我和她都很久沒有聯(lián)系了好么 哈 下次這種無事生非的輿論八卦 還是一笑置...
    夢小飛閱讀 142評論 0 0