我要在棧上漱挎。不系枪,你應(yīng)該在堆上

ixyP3XP.jpg

原文地址:我要在棧上。不识樱,你應(yīng)該在堆上

前言

我們?cè)趯懘a的時(shí)候嗤无,有時(shí)候會(huì)想這個(gè)變量到底分配到哪里了?這時(shí)候可能會(huì)有人說(shuō)怜庸,在棧上当犯,在堆上。信我準(zhǔn)沒(méi)錯(cuò)...

但從結(jié)果上來(lái)講你還是一知半解割疾,這可不行嚎卫,萬(wàn)一被人懵了呢。今天我們一起來(lái)深挖下 Go 在這塊的奧妙宏榕,自己動(dòng)手豐衣足食

問(wèn)題

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo() *User {
    return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}
}

func main() {
    _ = GetUserInfo()
}

開局就是一把問(wèn)號(hào)拓诸,帶著問(wèn)題進(jìn)行學(xué)習(xí)。請(qǐng)問(wèn) main 調(diào)用 GetUserInfo 后返回的 &User{...}麻昼。這個(gè)變量是分配到棧上了呢奠支,還是分配到堆上了?

什么是堆/棧

在這里并不打算詳細(xì)介紹堆棧抚芦,僅簡(jiǎn)單介紹本文所需的基礎(chǔ)知識(shí)倍谜。如下:

  • 堆(Heap):一般來(lái)講是人為手動(dòng)進(jìn)行管理,手動(dòng)申請(qǐng)叉抡、分配尔崔、釋放。一般所涉及的內(nèi)存大小并不定褥民,一般會(huì)存放較大的對(duì)象季春。另外其分配相對(duì)慢,涉及到的指令動(dòng)作也相對(duì)多
  • 棧(Stack):由編譯器進(jìn)行管理消返,自動(dòng)申請(qǐng)载弄、分配耘拇、釋放。一般不會(huì)太大宇攻,我們常見的函數(shù)參數(shù)(不同平臺(tái)允許存放的數(shù)量不同)驼鞭,局部變量等等都會(huì)存放在棧上

今天我們介紹的 Go 語(yǔ)言,它的堆棧分配是通過(guò) Compiler 進(jìn)行分析尺碰,GC 去管理的挣棕,而對(duì)其的分析選擇動(dòng)作就是今天探討的重點(diǎn)

什么是逃逸分析

在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動(dòng)態(tài)范圍的方法亲桥,簡(jiǎn)單來(lái)說(shuō)就是分析在程序的哪些地方可以訪問(wèn)到該指針

通俗地講洛心,逃逸分析就是確定一個(gè)變量要放堆上還是棧上,規(guī)則如下:

  1. 是否有在其他地方(非局部)被引用题篷。只要有可能被引用了词身,那么它一定分配到堆上。否則分配到棧上
  2. 即使沒(méi)有被外部引用番枚,但對(duì)象過(guò)大法严,無(wú)法存放在棧區(qū)上。依然有可能分配到堆上

對(duì)此你可以理解為葫笼,逃逸分析是編譯器用于決定變量分配到堆上還是棧上的一種行為

在什么階段確立逃逸

在編譯階段確立逃逸深啤,注意并不是在運(yùn)行時(shí)

為什么需要逃逸

這個(gè)問(wèn)題我們可以反過(guò)來(lái)想,如果變量都分配到堆上了會(huì)出現(xiàn)什么事情路星?例如:

  • 垃圾回收(GC)的壓力不斷增大
  • 申請(qǐng)溯街、分配、回收內(nèi)存的系統(tǒng)開銷增大(相對(duì)于棧)
  • 動(dòng)態(tài)分配產(chǎn)生一定量的內(nèi)存碎片

其實(shí)總的來(lái)說(shuō)洋丐,就是頻繁申請(qǐng)呈昔、分配堆內(nèi)存是有一定 “代價(jià)” 的。會(huì)影響應(yīng)用程序運(yùn)行的效率友绝,間接影響到整體系統(tǒng)堤尾。因此 “按需分配” 最大限度的靈活利用資源,才是正確的治理之道迁客。這就是為什么需要逃逸分析的原因郭宝,你覺(jué)得呢?

怎么確定是否逃逸

第一哲泊,通過(guò)編譯器命令剩蟀,就可以看到詳細(xì)的逃逸分析過(guò)程催蝗。而指令集 -gcflags 用于將標(biāo)識(shí)參數(shù)傳遞給 Go 編譯器切威,涉及如下:

  • -m 會(huì)打印出逃逸分析的優(yōu)化策略,實(shí)際上最多總共可以用 4 個(gè) -m丙号,但是信息量較大先朦,一般用 1 個(gè)就可以了

  • -l 會(huì)禁用函數(shù)內(nèi)聯(lián)缰冤,在這里禁用掉 inline 能更好的觀察逃逸情況,減少干擾

$ go build -gcflags '-m -l' main.go

第二喳魏,通過(guò)反編譯命令查看

$ go tool compile -S main.go

注:可以通過(guò) go tool compile -help 查看所有允許傳遞給編譯器的標(biāo)識(shí)參數(shù)

逃逸案例

案例一:指針

第一個(gè)案例是一開始拋出的問(wèn)題棉浸,現(xiàn)在你再看看,想想刺彩,如下:

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo() *User {
    return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}
}

func main() {
    _ = GetUserInfo()
}

執(zhí)行命令觀察一下迷郑,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:54: &User literal escapes to heap

通過(guò)查看分析結(jié)果,可得知 &User 逃到了堆里创倔,也就是分配到堆上了嗡害。這是不是有問(wèn)題啊...再看看匯編代碼確定一下,如下:

$ go tool compile -S main.go                
"".GetUserInfo STEXT size=190 args=0x8 locals=0x18
    0x0000 00000 (main.go:9)    TEXT    "".GetUserInfo(SB), $24-8
    ...
    0x0028 00040 (main.go:10)   MOVQ    AX, (SP)
    0x002c 00044 (main.go:10)   CALL    runtime.newobject(SB)
    0x0031 00049 (main.go:10)   PCDATA  $2, $1
    0x0031 00049 (main.go:10)   MOVQ    8(SP), AX
    0x0036 00054 (main.go:10)   MOVQ    $13746731, (AX)
    0x003d 00061 (main.go:10)   MOVQ    $7, 16(AX)
    0x0045 00069 (main.go:10)   PCDATA  $2, $-2
    0x0045 00069 (main.go:10)   PCDATA  $0, $-2
    0x0045 00069 (main.go:10)   CMPL    runtime.writeBarrier(SB), $0
    0x004c 00076 (main.go:10)   JNE 156
    0x004e 00078 (main.go:10)   LEAQ    go.string."EDDYCJY"(SB), CX
    ...

我們將目光集中到 CALL 指令畦攘,發(fā)現(xiàn)其執(zhí)行了 runtime.newobject 方法霸妹,也就是確實(shí)是分配到了堆上。這是為什么呢知押?

分析結(jié)果

這是因?yàn)?GetUserInfo() 返回的是指針對(duì)象叹螟,引用被返回到了方法之外了。因此編譯器會(huì)把該對(duì)象分配到堆上台盯,而不是棧上罢绽。否則方法結(jié)束之后,局部變量就被回收了静盅,豈不是翻車有缆。所以最終分配到堆上是理所當(dāng)然的

再想想

那你可能會(huì)想,那就是所有指針對(duì)象温亲,都應(yīng)該在堆上棚壁?并不。如下:

func main() {
    str := new(string)
    *str = "EDDYCJY"
}

你想想這個(gè)對(duì)象會(huì)分配到哪里栈虚?如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:4:12: main new(string) does not escape

顯然袖外,該對(duì)象分配到棧上了。很核心的一點(diǎn)就是它有沒(méi)有被作用域之外所引用魂务,而這里作用域仍然保留在 main 中曼验,因此它沒(méi)有發(fā)生逃逸

案例二:未確定類型

func main() {
    str := new(string)
    *str = "EDDYCJY"

    fmt.Println(str)
}

執(zhí)行命令觀察一下,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:13: str escapes to heap
./main.go:6:12: new(string) escapes to heap
./main.go:9:13: main ... argument does not escape

通過(guò)查看分析結(jié)果粘姜,可得知 str 變量逃到了堆上鬓照,也就是該對(duì)象在堆上分配。但上個(gè)案例時(shí)它還在棧上孤紧,我們也就 fmt 輸出了它而已豺裆。這...到底發(fā)生了什么事?

分析結(jié)果

相對(duì)案例一,案例二只加了一行代碼 fmt.Println(str)臭猜,問(wèn)題肯定出在它身上躺酒。其原型:

func Println(a ...interface{}) (n int, err error)

通過(guò)對(duì)其分析,可得知當(dāng)形參為 interface 類型時(shí)蔑歌,在編譯階段編譯器無(wú)法確定其具體的類型羹应。因此會(huì)產(chǎn)生逃逸,最終分配到堆上

如果你有興趣追源碼的話次屠,可以看下內(nèi)部的 reflect.TypeOf(arg).Kind() 語(yǔ)句园匹,其會(huì)造成堆逃逸,而表象就是 interface 類型會(huì)導(dǎo)致該對(duì)象分配到堆上

案例三劫灶、泄露參數(shù)

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo(u *User) *User {
    return u
}

func main() {
    _ = GetUserInfo(&User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"})
}

執(zhí)行命令觀察一下偎肃,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:18: leaking param: u to result ~r1 level=0
./main.go:14:63: main &User literal does not escape

我們注意到 leaking param 的表述,它說(shuō)明了變量 u 是一個(gè)泄露參數(shù)浑此。結(jié)合代碼可得知其傳給 GetUserInfo 方法后累颂,沒(méi)有做任何引用之類的涉及變量的動(dòng)作,直接就把這個(gè)變量返回出去了凛俱。因此這個(gè)變量實(shí)際上并沒(méi)有逃逸紊馏,它的作用域還在 main() 之中,所以分配在棧上

再想想

那你再想想怎么樣才能讓它分配到堆上蒲犬?結(jié)合案例一朱监,舉一反三。修改如下:

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo(u User) *User {
    return &u
}

func main() {
    _ = GetUserInfo(User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"})
}

執(zhí)行命令觀察一下原叮,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:9: &u escapes to heap
./main.go:9:18: moved to heap: u

只要一小改赫编,它就考慮會(huì)被外部所引用,因此妥妥的分配到堆上了

總結(jié)

在本文我給你介紹了逃逸分析的概念和規(guī)則奋隶,并列舉了一些例子加深理解擂送。但實(shí)際肯定遠(yuǎn)遠(yuǎn)不止這些案例,你需要做到的是掌握方法唯欣,遇到再看就好了嘹吨。除此之外你還需要注意:

  • 靜態(tài)分配到棧上,性能一定比動(dòng)態(tài)分配到堆上好
  • 底層分配到堆境氢,還是棧蟀拷。實(shí)際上對(duì)你來(lái)說(shuō)是透明的,不需要過(guò)度關(guān)心
  • 每個(gè) Go 版本的逃逸分析都會(huì)有所不同(會(huì)改變萍聊,會(huì)優(yōu)化)
  • 直接通過(guò) go build -gcflags '-m -l' 就可以看到逃逸分析的過(guò)程和結(jié)果
  • 到處都用指針傳遞并不一定是最好的问芬,要用對(duì)

之前就有想過(guò)要不要寫 “逃逸分析” 相關(guān)的文章,直到最近看到在夜讀里有人問(wèn)寿桨,還是有寫的必要此衅。對(duì)于這塊的知識(shí)點(diǎn)。我的建議是適當(dāng)了解,但沒(méi)必要硬記炕柔。靠基礎(chǔ)知識(shí)點(diǎn)加命令調(diào)試觀察就好了媒佣。像是曹大之前講的 “你琢磨半天逃逸分析匕累,一壓測(cè),瓶頸在鎖上”默伍,完全沒(méi)必要過(guò)度在意...

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末欢嘿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子也糊,更是在濱河造成了極大的恐慌炼蹦,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狸剃,死亡現(xiàn)場(chǎng)離奇詭異掐隐,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)钞馁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門虑省,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人僧凰,你說(shuō)我怎么就攤上這事探颈。” “怎么了训措?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵伪节,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我绩鸣,道長(zhǎng)怀大,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任呀闻,我火速辦了婚禮叉寂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘总珠。我一直安慰自己屏鳍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布局服。 她就那樣靜靜地躺著钓瞭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淫奔。 梳的紋絲不亂的頭發(fā)上山涡,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼鸭丛。 笑死竞穷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鳞溉。 我是一名探鬼主播瘾带,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼熟菲!你這毒婦竟也來(lái)了看政?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤抄罕,失蹤者是張志新(化名)和其女友劉穎允蚣,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呆贿,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嚷兔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了做入。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谴垫。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖母蛛,靈堂內(nèi)的尸體忽然破棺而出翩剪,到底是詐尸還是另有隱情,我是刑警寧澤彩郊,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布前弯,位于F島的核電站,受9級(jí)特大地震影響秫逝,放射性物質(zhì)發(fā)生泄漏恕出。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一违帆、第九天 我趴在偏房一處隱蔽的房頂上張望浙巫。 院中可真熱鬧,春花似錦刷后、人聲如沸的畴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)丧裁。三九已至,卻和暖如春含衔,著一層夾襖步出監(jiān)牢的瞬間煎娇,已是汗流浹背二庵。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缓呛,地道東北人催享。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像哟绊,于是被迫代替她去往敵國(guó)和親因妙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • 第二部分 自動(dòng)內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,159評(píng)論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,096評(píng)論 1 32
  • 前言 記錄個(gè)人在2017年08月的學(xué)習(xí)和總結(jié)匿情,不定期更新 2017-08-02 有序的Map HashMap是無(wú)序...
    Kevin_ZGJ閱讀 404評(píng)論 0 0
  • 第6章類文件結(jié)構(gòu) 6.1 概述 6.2 無(wú)關(guān)性基石 6.3 Class類文件的結(jié)構(gòu) java虛擬機(jī)不和包括java...
    kennethan閱讀 925評(píng)論 0 2
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理兰迫,因此不免有一些不準(zhǔn)確的地方信殊,同時(shí)不同JDK版本的...
    高廣超閱讀 15,595評(píng)論 3 83