Go逃逸分析示例詳解
引言大綱
這個月我會整理分享一系列后端工程師求職面試相關(guān)的文章,知識脈絡(luò)圖如下:
- JAVA/GO/PHP 面試常問的知識點
- DB:MySql PgSql
- Cache: Redis MemCache MongoDB
- 數(shù)據(jù)結(jié)構(gòu)
- 算法
- 微服務(wù)&高并發(fā)
- 流媒體
- WEB3.0
- 源碼分析
通過這一系列的文章,大家不僅能復(fù)習(xí)梳理后端開發(fā)相關(guān)的知識點,也可以了解目前的市場環(huán)境對服務(wù)端開發(fā),尤其是對Go開發(fā)工程師的崗位要求,需要掌握哪些核心技術(shù)。
上一篇文章: 【狂刷面試題】GO常見面試題匯總我們介紹了:切片相關(guān)的知識點;深拷貝和淺拷貝的區(qū)別;new和make的區(qū)別;map的底層實現(xiàn)是hash,默認(rèn)不支持排序,我們可以通過什么思路來實現(xiàn)map有序取值;值類型和引用類型的區(qū)別;GO語言中堆和棧的區(qū)別,什么數(shù)據(jù)會分配到堆中,什么變量會分配到棧中;
感興趣的同學(xué)可以先看上一篇文章,能更好的理解這篇介紹的硬核知識點:逃逸分析。
逃逸分析
我們在之前有提到堆和棧的概念,要搞清楚GO的逃逸分析一定要先搞清楚堆棧的特點:
正如我們上面提到的,內(nèi)存分配既可以分配到堆中,也可以分配到棧中。
那么什么樣的數(shù)據(jù)會被分配到棧中,什么樣的數(shù)據(jù)又會被分配到堆中呢?GO語言是如何進(jìn)行內(nèi)存分配的呢?其設(shè)計初衷和實現(xiàn)原理是什么呢?
我們先來了解一下內(nèi)存管理、堆、棧的知識點:
內(nèi)存管理
內(nèi)存管理主要包括兩個動作:分配與釋放。逃逸分析就是服務(wù)于內(nèi)存分配,為了更好理解逃逸分析,我們再來回顧一下堆棧的特點:
棧
在Go中,棧的內(nèi)存是由編譯器自動進(jìn)行分配和釋放,棧區(qū)往往存儲著函數(shù)參數(shù)、局部變量和調(diào)用函數(shù)幀,它們隨著函數(shù)的創(chuàng)建而分配,函數(shù)的退出而銷毀。
一個goroutine對應(yīng)一個棧,棧是調(diào)用棧(call stack)的簡稱。一個棧通常又包含了許多棧幀(stack frame),它描述的是函數(shù)之間的調(diào)用關(guān)系,每一幀對應(yīng)一個尚未返回的函數(shù)調(diào)用,它本身也是以棧形式存放數(shù)據(jù)。
堆
與棧不同的是,應(yīng)用程序在運(yùn)行時只會存在一個堆。
我們可以簡單理解為:我們在GO開發(fā)過程中要考慮的內(nèi)存管理只是針對堆內(nèi)存而言的。
程序在運(yùn)行期間可以主動從堆上申請內(nèi)存,這些內(nèi)存通過Go的內(nèi)存分配器分配,并由垃圾收集器回收。
堆和棧的對比
加鎖
棧不需要加鎖:棧是每個goroutine獨有的,這就意味著棧上的內(nèi)存操作是不需要加鎖的。
堆有時需要加鎖:堆上的內(nèi)存,有時需要加鎖防止多線程沖突
延伸知識點:為什么堆上的內(nèi)存有時需要加鎖?而不是一直需要加鎖呢?
因為Go的內(nèi)存分配策略學(xué)習(xí)了TCMalloc的線程緩存思想,他為每個處理器P分配了一個mcache,從mcache分配內(nèi)存也是無鎖的
性能
- 堆內(nèi)存管理 性能差:對于程序堆上的內(nèi)存回收,還需要通過標(biāo)記清除階段,例如Go采用的三色標(biāo)記法。
- 棧內(nèi)存管理 性能好:棧上的內(nèi)存,它的分配與釋放非常高效的。簡單地說,它只需要兩個CPU指令:一個是分配入棧,另外一個是棧內(nèi)釋放。只需要借助于棧相關(guān)寄存器即可完成。
緩存策略
- 棧緩存性能更好
- 堆緩存性能較差
原因是:棧內(nèi)存能更好地利用CPU的緩存策略,因為??臻g相較于堆來說是更連續(xù)的。
逃逸分析優(yōu)勢
上面說了這么多堆和棧的知識點,目的是為了讓大家更好的理解逃逸分析。
正如我們講的,相比于把內(nèi)存分配到堆中,分配到棧中優(yōu)勢更明顯。
Go語言也是這么做的:Go編譯器會盡可能將變量分配到到棧上。
但是,當(dāng)編譯器無法證明函數(shù)返回后,該變量沒有被引用,那么編譯器就必須在堆上分配該變量,以此避免懸掛指針(dangling pointer)。另外,如果局部變量非常大,也會將其分配在堆上。
Go是如何確定內(nèi)存是分配到棧上還是堆上的呢?
答案就是:逃逸分析。
編譯器通過逃逸分析技術(shù)去選擇堆或者棧,逃逸分析的基本思想如下:檢查變量的生命周期是否是完全可知的,如果通過檢查,則在棧上分配。否則,就是所謂的逃逸,必須在堆上進(jìn)行分配。
逃逸分析原則
Go語言雖然沒有明確說明逃逸分析原則,但是有以下幾點準(zhǔn)則,是可以參考的。
- 不同于jvm的運(yùn)行時逃逸分析,Go的逃逸分析是在編譯期完成的:編譯期無法確定的參數(shù)類型必定放到堆中;
- 如果變量在函數(shù)外部沒有引用,則優(yōu)先放到棧中;
- 如果變量在函數(shù)外部存在引用,則必定放在堆中;
- 如果變量占用內(nèi)存較大時,則優(yōu)先放到堆中;
逃逸分析舉例
我們使用這個命令來查看逃逸分析的結(jié)果: go build -gcflags '-m -m -l'
1.參數(shù)是interface類型
package main import "fmt" func main() { a := 666 fmt.Println(a) }
運(yùn)行結(jié)果
原因分析
因為Println(a ...interface{})的參數(shù)是interface{}類型,編譯期無法確定其具體的參數(shù)類型,所以內(nèi)存分配到堆中。
2. 變量在函數(shù)外部有引用
package main func test() *int { a := 10 return &a } func main() { _ = test() }
運(yùn)行結(jié)果
原因分析
變量a在函數(shù)外部存在引用。
我們來分析一下執(zhí)行過程:當(dāng)函數(shù)執(zhí)行完畢,對應(yīng)的棧幀就被銷毀,但是引用已經(jīng)被返回到函數(shù)之外。如果這時外部通過引用地址取值,雖然地址還在,但是這塊內(nèi)存已經(jīng)被釋放回收了,這就是非法內(nèi)存。
在這種情況下必須分配到堆上。
3. 變量內(nèi)存占用較大
package main func test() { a := make([]int, 10000, 10000) for i := 0; i < 10000; i++ { a[i] = i } } func main() { test() }
運(yùn)行結(jié)果
原因分析
我們定義了一個容量為10000的int類型切片,內(nèi)存分配到了棧上。
我們再簡單修改一下代碼,將切片的容量和長度修改為1,再次查看逃逸分析的結(jié)果,我們發(fā)現(xiàn),沒有發(fā)生逃逸,內(nèi)存默認(rèn)分類到了棧上。
所以,當(dāng)變量占用內(nèi)存較大時,會發(fā)生逃逸分析,將內(nèi)存分配到堆上。
4. 變量大小不確定時
我們再簡單修改一下上面的代碼:
package main func test() { l := 1 a := make([]int, l, l) for i := 0; i < l; i++ { a[i] = i } } func main() { test() }
運(yùn)行結(jié)果
原因分析
我們通過控制臺的輸出結(jié)果可以很明顯的看出:發(fā)生了逃逸,分配到了heap堆中。
原因是這樣的:
我們雖然在代碼段中給變量 l 賦值了1,但是編譯期間只能識別到初始化int類型切片時,傳入的長度和容量是變量l,編譯器并不能確定變量l的值,所以發(fā)生了逃逸,會把內(nèi)存分配到堆中。
思考題
好了,我們舉了4個逃逸分析的經(jīng)典案例,相信聰明的你已經(jīng)理解了逃逸分析的作用和發(fā)生逃逸的場景。
我們來想一下,在理解逃逸分析的原理之后,在開發(fā)的過程中如何更好的編碼,進(jìn)而提高程序的效率,更好的利用內(nèi)存呢?
如何實踐?
理解逃逸分析一定能幫助我們寫出更好的程序。知道變量分配在棧堆之上的差別后,我們就要盡量寫出分配在棧上的代碼。因為堆上的變量變少后,可以減輕內(nèi)存分配的開銷,減小GC的壓力,提高程序的運(yùn)行速度。
但是我們也要有過猶不及的指導(dǎo)思想。
我認(rèn)為沒有一成不變的開發(fā)模式,我們一定是在不斷的需求變化,業(yè)務(wù)變化中求得平衡的:
舉個日常開發(fā)中函數(shù)傳參栗子:
有些場景下我們不應(yīng)該傳遞結(jié)構(gòu)體指針,而應(yīng)該直接傳遞結(jié)構(gòu)體。
為什么會這樣呢?雖然直接傳遞結(jié)構(gòu)體需要值拷貝,但是這是在棧上完成的操作,開銷遠(yuǎn)比變量逃逸后動態(tài)地在堆上分配內(nèi)存少的多。
當(dāng)然這種做法不是絕對的,要根據(jù)場景去分析:
- 如果結(jié)構(gòu)體較大,傳遞結(jié)構(gòu)體指針更合適,因為指針類型相比值類型能節(jié)省大量的內(nèi)存空間
- 如果結(jié)構(gòu)體較小,傳遞結(jié)構(gòu)體更適合,因為在棧上分配內(nèi)存,可以有效減少GC壓力
總結(jié)
通過本文的介紹,相信你一定加深了堆棧的理解;搞清楚逃逸分析的作用和原理之后能夠指導(dǎo)我們寫出更優(yōu)雅的代碼。
我們在日常開發(fā)中,要根據(jù)實際場景考慮,如何將內(nèi)存盡量分配到棧中,減少GC的壓力,提高性能。
如何找到應(yīng)用開發(fā)效率,程序運(yùn)行效率,對機(jī)器的壓力及負(fù)載的平衡點,是程序員進(jìn)階之旅中的必修課。
以上就是Go逃逸分析示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Go逃逸分析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用systemd部署golang項目的實現(xiàn)方法
這篇文章主要介紹了利用systemd部署golang項目的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11GO語言協(xié)程創(chuàng)建使用并通過channel解決資源競爭
這篇文章主要為大家介紹了GO語言協(xié)程創(chuàng)建使用并通過channel解決資源競爭,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04