一. array與slice
對於一些初學者,自知道 Go 裡面的 array 以 pass-by-value 方式傳遞後,就莫名地引起 “恐慌”。外加諸多文章未作說明,就建議用 slice 代替 array,企圖避免資料拷貝,提升效能。實際上,此做法有待商榷。某些時候怕會適得其反,倒造成不必要的效能損失。
用個簡單的樣本說明。
package mainimport ("fmt")const capacity = 1024func array() [capacity]int {var d [capacity]intfor i := 0; i < len(d); i++ {d[i] = 1}return d}func slice() []int {d := make([]int, capacity)for i := 0; i < len(d); i++ {d[i] = 1}return d}func main() {fmt.Println(array())fmt.Println(slice())}
代碼很簡單,兩個函數分別返回 “內容相同” 的 array 和 slice。為避免編譯器最佳化,特填充了全部資料,以類比 “真實” 資料複製行為。接下來,看看效能測試對比。
package mainimport ("testing")func BenchmarkArray(b *testing.B) {for i := 0; i < b.N; i++ {_ = array()}}func BenchmarkSlice(b *testing.B) {for i := 0; i < b.N; i++ {_ = slice()}}
這結果怕是顛覆了最初認知。array 非但擁有更好的效能,還避免了堆記憶體配置,也就是說減輕了 GC 壓力。為什麼會這樣。
熟悉彙編的,怕是很容易看出來。函數 array 傳回值的複製只需用 "CX + REP" 指令就可完成。
整個 array 函數完全在棧上完成,而 slice 函數則需執行 makeslice,繼而在堆上分配記憶體,這就是問題所在。
對於一些短小的對象,複製成本遠小於在堆上分配和回收操作。 Go Proverbs: A little copying is better than a little dependency.
二. defer
編譯器通過 runtime.deferproc “註冊” 延遲調用,除目標函數地址外,還會複製相關參數(包括 receiver)。在函數返回前,執行runtime.deferreturn 提取相關資訊執行延遲調用。這其中的代價自然不是普通函數調用一條 CALL 指令所能比擬的。
解決方案麼,要麼去掉 f.close 前的 defer,要麼將內層處理邏輯重構為獨立函數(比如匿名函數調用)。