本篇是根據 GopherCon SG 2019 “Understanding Allocations” 演講的學習筆記,
Understanding Allocations: the Stack and the Heap - GopherCon SG 2019 - YouTube
理解分配:堆疊和堆

你的程式中有兩種記憶體,堆疊記憶體和堆記憶體,
go 中,每個 go 程都會有一個堆疊空間,整個程式有一個堆空間,
變數是在堆疊還是堆上

負責堆垃圾回收的 GC 會導致整個程式的延遲,而不僅僅是創建垃圾的部分,你可能會擔心你的代碼在堆中產生了多少垃圾,
什么時候需要優化

要有 benchmarks 基準來證明你的程式不夠快(有大量的堆記憶體分配),夠快就不用多此一舉了,
你要先確保程式能正確運作(業務處理),而不是先著重性能優化,
在有大量指標+運行快速的程式、清晰明了+但有點慢的程式之間,你應該選擇后者,
普通型別引數傳遞
第5行進入squre函式

函式和區域變數被擠壓入堆疊,一個函式為一個堆疊幀,
squre函式執行完

執行完成后,你會發現黑線(只是用于區分)向上移,上方內容為有效內容,下方內容為無效內容(過期而不再使用)
第6行執行println函式

go 宣告了新的記憶體部分,我們有了新的堆疊幀用于列印行,黑線下移,
通俗的來講,堆疊空間會進行自我清理,任何變數都會被清理干凈,空間會被重復使用,
指標型別引數傳遞
第4行宣告變數

main 函式和變數 n 被壓入堆疊,
第5行進入inc函式

inc 函式和變數 x 被壓入堆疊,屬于另一個堆疊幀,黑線下移,傳入的變數是 n 的地址,
inc函式執行完

黑線上移,n 被修改了值,并沒有什么問題,
執行println函式

總結
雖然使用了指標傳參,但這種情況下它能夠留在堆疊上,即共享向下傳遞時,通常留在堆疊空間上,(主函式將自己的變數傳給子函式)
函式回傳參考
第4行進入answer函式前

編譯器推斷出 n 是 int 的指標型別,賦初值 nil 等待函式回傳結果,
進入函式

在函式內部宣告了變數并初始化,并回傳了變數的記憶體地址,
執行println函式

致命的問題
你會發現,我明明沒有進行賦值修改的操作,但卻破壞了原有的值,原因在于
- 呼叫
answer函式的時候,假如我們把宣告的變數x放在堆疊空間,回傳變數的地址, - 那么
main函式中的n會拿到x的地址,當answer方法退出時(黑線上移),該堆疊幀已過期,但是n還參考著x, - 我們知道堆疊的空間是可以被重復利用的,那么下一次執行其他函式或者宣告變數的時候,那塊過期的記憶體區域就會被重新使用,修改為其他值,
- 案例中呼叫
println,黑線下移,產生新的堆疊幀,原先x識別符號被替換成了a,修改a的值等同修改了n地址解參考后的值,這是非常致命的錯誤,
解決的方法

為了解決上述致命的問題,被宣告初始化的 x 變數就需要“逃逸”到堆空間中,這樣其他無關函式就不會對它的值產生影響,
共享向上傳遞時,通常留在堆空間上,(子函式將自己的變數傳給主函式,參考)
Escape Analysis
編譯器怎么判斷的


變數只在函式里作業,那就分配到堆疊空間上,如果編譯器不能證明函式回傳后宣告過的變數是否被參考,那必須將其分配到堆空間上,
詢問編譯器

查看構建指令,可以提供一個 -gcflags 的可選引數,傳遞給 go tool compile 工具

查詢 go tool compile -h,得知引數 -m 可以輸出編譯器的優化決定(變數放在堆疊還是堆上)

執行 go build -gcflags "-m" 獲取到下面的結果


總結


什么時候會把變數分配在堆記憶體上
- 函式回傳退出后宣告過的變數依舊被參考著,
- 變數初始化值大小過大無法分配進堆疊,
- 不知道變數值的具體大小,比如切片,
做個判斷

看看前面總結的第一點和第三點,明顯可以知道右邊那個是分配在堆疊上,而左邊那個分配在堆上,
思考io.Reader介面

io 標準庫里有個 Reader 介面,你應該知道為什么官方要用前者替換后者了吧,如果使用后者,會在堆上產生大量的垃圾,造成程式遲鈍,前者則符合向下傳遞思想,變數通常分配在堆疊空間上,
最后

不要猜想,多用工具!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/455389.html
標籤:Go
上一篇:背包問題(5):混合背包
下一篇:Java中的基本資料型別
