大家好,我是煎鱼。
最近 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][1] 中分享的 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[2] 项目中实际的性能对比。
没有用 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。外部需求就畏畏缩缩了。真双标?
推荐阅读
加大力度!Go 将会增强 Go1 向后兼容性 打脸了兄弟们,Go1.20 arena 来了! Go 十年了,终于想起要统一 log 库了!
参考资料
Golang memory arenas [101 guide]: https://uptrace.dev/blog/golang-memory-arena.html
[2]vmihailenco/golang-memory-arenas: https://github.com/vmihailenco/golang-memory-arenas/commit/3e88fc3cd3054e214fd401f8d434ee04fd476d2b