Escape Analysis是什麼?簡單的說,就是本來預期要放在 stack 上的資料,卻被放到 heap 上去了。
在開發程式裡,一樣的資料可以放在 stack 或是 heap,但在效率上,正常情況下放在 stack 裡來使用效率都會優於 heap。所以有些變數的使用,如果能避免往 heap 裡放,對程式來說都會是一種效能的提升。
只是有一些看起來很直覺該放在 stack 上的程式寫法,卻會在編譯時卻有可能會被放到 heap 上去。比如像下面的程式(只是示意,但聰明的編譯器可能會處理掉,底下會提)。
var pn *int
func main() {
foo()
}
func foo() {
n := 10
pn = &n
}
編譯器為什麼要把資料從 stack 搬到 heap?因為當編譯器在編譯時發現,某個變數所指向的資料在當前 scope 結束後還是可能會被繼續使用時,編譯器就會決定把這份資料搬到 heap,以避免等一下要用的人找不到。
用上面的範例程式來看,foo 這個方法結束時,它的 stack frame 也會跟著結束,裡面的 n 變數會被回收,但因為 pn 指標還在繼續指向 n,所以編譯器會把 n 這個資料搬到 heap 上,讓 foo 方法就算結束,外面的 pn 這個 point 還是可以指向放在 heap 上的資料,不會找不到。
常見的 fmt.Println(num) or fmt.Print(num) 這些系列的方法,可以看到他們接的參數都是用別名 any 來接,而 any 指的就是 interface。
當我們把數字型別轉成 interface 時,interface 裡有個欄位本身就是 pointer 來指向實際的資料。假如編譯器判定所使用的變數沒有 escape 的可疑跡象,那 interface 這個 pointer 就會指向 stack frame 上的資料;但假如編譯器判定很可疑的時候,那這筆資料會被搬到 heap,然後 interface 裡的 pointer 就會指向 heap 上面的資料。
所以以 fmt.Println(num) 為例,他收的參數是 interface,所以傳入的數字會先被轉為 interface(這裡面就會有指標了),接著呼叫 fmt.Println(num) 是有跨 package & 用到 reflection。這些都會讓編譯器無法確定 interface 所指向 stack frame 上的資料是否還會被繼續使用,所以避免免危險,乾脆就把資料搬到 heap 上。
以開頭的那段範例程式碼,其實不一定會發生 escape 的現象。聰明的編譯器在編譯時,它可能會判定所呼叫的方法太過傻瓜版。所以編譯器會直接把 function 裡的程式碼"copy-paste"到呼叫方。因為編譯器判定把程式碼 copy-paste 的效益,大於呼叫 function 本身的成本。這個就是所謂的 function inline(內聯)。
看了一些資料,總結自己以後在寫程式時,基本上會用傻瓜版二分法。如果 pointer 的值在離開一個 scope(或叫 closure)之後還可能會被繼續使用,那這筆資料會中 escape 的機會就比較高。
其實中 escape 對效能有多少影響,每個程式跟場景都說不準。
假如在 review 既有程式已經發現它就是有 escape 的問題,但這段程式認真講,也不是所謂的 hot path(熱路徑),那硬要把它 tune 掉的效益可能也不會太大。
只是身為一個 IT,當我們自己在寫程式或 review AI 所寫出來的程式時,分清楚資料是放在 stack 還是 heap 會很有幫助。
避免讓資料被放到 heap 上總是能少掉 allocat, gc 等這些麻煩事,盡量放在對 cpu 使用友善的 stack 上,大方向總是不會錯的。
go build 的時候可以加上 gcflags="-m" 來掃一下有沒有 escape 的情況。
go build -gcflags="-m" main.go