大家好,我是煎魚。
最近 Go1.20 中的手動管理記憶體受到了很多人的關注。眾所周知,Go 是一門帶垃圾回收(GC)的程式語言,可以進行自動的記憶體申請、釋放等記憶體操作。
帶 GC 能簡化程式設計時的心智成本,也保證了記憶體的安全。我們說 “一般”,也就是有例外。人們說六個,一般都有七個。
Go 的例外就出現了。
Go1.20 arena
新版本 Go1.20,基於 Google 自身的需求,快速透過了實踐,正式支援了 arena,能夠實現手動的記憶體管理(當前是實驗性特性)。
現在可以透過 GOEXPERIMENT=arenas
環境變數啟用:
GOEXPERIMENT=arenas go run main.go
該特性可以讓程式設計師手動的從一個連續的記憶體區域申請、分配一組記憶體物件,也可以一次性的釋放。
重點是可以手動管理記憶體。
提供的 arena API
- NewArena:建立一個新的 arena 記憶體空間。
- Free:釋放 arena 及其關聯物件。
- New:基於 arena,建立新物件。
- MakeSlice:基於 arena,建立新切片。
- Clone:克隆一個 arena 的物件,並移動到記憶體堆上。
一些 arena 例子
以下案例和效能測試是基於 uptrace 在 Golang memory arenas [101 guide] 中分享的 arena 例子,本處進行引用,我就不自創一份了。
很適合在初學時作為 Demo 使用,打算也留著自己下次用時結合文件翻一番。
arena.NewArena
一起來快速入門。程式碼如下:
import "arena"
type T struct{
Foo string
Bar [16]byte
}
func processRequest(req *http.Request) {
// 在函式開頭建立一個 arena
mem := arena.NewArena()
// 在函式結束時釋放 arena
defer mem.Free()
// 從申請的 arena 中申請一些物件
for i := 0; i < 10; i++ {
obj := arena.New[T](mem "T")
}
// 從申請的 arena 中申請切片物件(指定長度和容量)
slice := arena.MakeSlice[T](mem, 100, 200 "T")
}
arena.Clone
如果要單獨使用某個申請出來的物件。可以藉助 Clone 方法進行單獨處理。
如下程式碼:
// 建立一個 arena
mem := arena.NewArena()
obj1 := arena.New[T](mem "T") // 分配一個 arena 物件
obj2 := arena.Clone(obj1) // 複製一個 arena 上的物件,移動到記憶體堆上
fmt.Println(obj2 == obj1) // 即使是基於複製出來的,兩者並不完全等價
// 釋放 arena,obj1 不可使用,obj2 可正常使用
mem.Free()
釋放了最早申請的 arena,Clone 方法在這裡將會把 obj1 複製到新的記憶體堆上,再賦值給 obj2。後續要單獨用 obj2 就可以繼續使用。
reflect.ArenaNew
也可以結合 arena 和 reflect 兩個標準庫來進行使用。如下程式碼:
var typ = reflect.TypeOf((*T)(nil)).Elem()
mem := arena.NewArena()
defer mem.Free()
value := reflect.ArenaNew(mem, typ)
fmt.Println(value.Interface().(*T))
arena.MakeSlice
該方法的常規用法:
arena.MakeSlice[string](mem, length, capacity "string")
如果需要申請一個新切片並追加元素:
slice := arena.MakeSlice[string](mem, 0, 0 "string")
slice = append(slice, "")
需要注意的是,arena 目前不支援 map。但你可以透過泛型來實現類似的效果。
arena.String
原則上 arena 不支援 string。但是我們依然可以透過 unsafe.String 方法的騷操作來變相實現。
如下程式碼:
src := "腦子進煎魚了"
mem := arena.NewArena()
defer mem.Free()
bs := arena.MakeSlice[byte](mem, len(src "byte"), len(src))
copy(bs, src)
str := unsafe.String(&bs[0], len(bs))
在申請的 arena 釋放後,該對應的 string 就無法使用了,需要特別注意。
效能表現
這個允許手工管理記憶體的 arena 的特性是來源於內部,提案也是一路綠燈透過。(懂得懂)。
自述已經為 Google 許多應用節省了高達 15% 的 CPU 和記憶體使用量,主要原因是減少了垃圾收集 CPU 時間和堆記憶體使用量。
經過在 vmihailenco/golang-memory-arenas 專案中實際的效能對比。
沒有用 arena:
/usr/bin/time go run arena_off.go
77.27user 1.28system 0:07.84elapsed 1001%CPU (0avgtext+0avgdata 532156maxresident)k
30064inputs+2728outputs (551major+292838minor)pagefaults 0swaps
使用了 arena:
GOEXPERIMENT=arenas /usr/bin/time go run arena_on.go
35.25user 5.71system 0:05.09elapsed 803%CPU (0avgtext+0avgdata 385424maxresident)k
48inputs+3320outputs (417major+63931minor)pagefaults 0swaps
使用了 arena 的程式碼執行速度更快,且使用的記憶體更少。
總結
Go 的各位大大們在效能最佳化中,不斷地試圖壓榨 Go 的潛力。現在已經到了手工管理記憶體的階段了。
實際的測試結果來看,是有作用的。
有興趣的小夥伴可以在 Go1.20 起就開始試用。不過需要注意,該特性由於發現了嚴重的 API 問題(想把 arena 應用到其他的標準庫中,但這是個大事件),社群還需要認真思考後續的發展。現階段處於處於停滯狀態。
從這次提案來看,真的是,內部需求一路猛如虎,直接衝上 master。外部需求就畏畏縮縮了。真雙標?