golang 1.20引入新特性arena,支持手动分配和释放内存,初步测试性能提升5%-15%甚至更多,真的是起飞了!尽管目前还是实验特性,对于泛型和反射reflect都已支持相当完善,常用场景比如JSON解析/ProtoBuf反序列化都会产生不少提升,本文带你提前全方位解析arena,并给出一些实际优化实践,早学早享受!

要是本文对您有帮助的话,欢迎【关注】作者,【点赞】+【收藏】,保持交流!

基础原理

尽管直观上我们认为arena要把Go变成C++了,实际上arena只是一个内存池的技术——创建一个大的连续内存块,该内存块只需要预先分配一次,然后在此内存上创建对象,使用完后统一释放内存。
如下,相比不使用arena,业务(JSON解析等)存在大量小对象,GC会消耗大量CPU和内存来实现垃圾回收,而使用arena只需要分配一次内存,所有对象都在池中管理,手动选择合适的时机释放。



简单使用

开启arena

目前还是实验特性,可如下任意开启

  • 定义环境变量: export GOEXPERIMENT=arenas
  • 运行程序同时开启: GOEXPERIMENT=arenas go run main.go
  • 指定Build Tag: go run main.go -tags goexperiment.arenas

编写相关代码,可在需要开启arena特性文件增加 //go:build goexperiment.arenas。具体使用很简单,先创建arena池,然后在此池上分配变量,使用完后统一释放arena池。

使用步骤

1.创建arena内存池,不需要的时候释放

  • NewArena(): 创建一个Arena, 你可以创建多个Arena, 批量创建一批对象,统一手工释放。它不是线程安全的。
  • Free(): 释放Arena以及它上面创建出来的所有的对象。释放的对象你不应该再使用了,否则可能会导致意想不到的错误。

2.从池中分配需要的空间
当前只支持具体对象和slice,还没有实现MakeMap、MakeChan这样在Arena上创建map和channel的方法,后续可能会加上。

3.如果希望内存池被释放后还使用,可拷贝到堆分配空间上

演示代码

最佳实践

判断arena是否适用

通过profile查看当前瓶颈

  • 内存

大部分内存分配allocations (65% 533.30 M)集中在代码InsertStackA处,可以优化,继续查看此处CPU的占用


  • cpu

InsertStackA占用大量CPU(43%),有不少计算消耗
不同地方的runtime.mallocgc累加占用14% CPU,runtime.gcBgMarkWorker占用5%,GC累计就占了19%。


综合判断,内存分配和GC都是瓶颈,使用area应该可以优化。

改进方式

参考这里的提交,通过封装一个slices or structs分配器从arena分配代替make分配,性能整体提升8%,runtime.mallocgc全部被换成runtime.(*userArena).alloc,gcBgWorker减半。


防止错误

和C++一样,自己管理内存,实际中最容易遇到的问题

  1. 忘记释放内存,导致OOM
  2. 引用已经释放的arena池上分配的遍历,导致程序Crash

通常用的手段是,程序实际上线前,借助一些地址/内存预检测手段,常用的就是address sanitizer (asan)/memory sanitizer (msan)。

如下引用释放arena池中变量

如下操作,可以看到对应的错误

生产建议

  • 不要滥用,和对待unsafe, reflect, or cgo一样,只有必要时用
  • 注意释放Free,需要释放后使用的记得Clone
  • 实际封装,可以全局封装一个多个持有arena池的单实例对象,或者参考鸟窝大佬的做法,类似context,每个函数传递一个全局分配好的arena池

对比sync.Pool

原理区别

同样都是为了解决频繁分配对象和大量对象GC带来的开销

  • sync.Pool

相同类型的对象,使用完后暂时缓存,不GC,下次再有相同的对象分配时直接用之前的缓存的对象,这样避免频繁创建大量对象。
不承诺这些缓存对象的生命周期,GC时会释放之前的缓存,适合解决频繁创建相同对象带来的压力,短时间(两次GC之间)大量创建可能还是会有较大冲击,使用相对简单,但只能用于相同结构创建,不能创建slice等复杂结构

  • arena

自己管理内存分配,统一手动释放,对象的生命周期完全自己控制,使用相对复杂,支持slice等复杂结构且可定制性强

性能对比

如下测试,对比测试创建对象的benchmark

GOEXPERIMENT=arenas go test -bench=. arena_test.go运行,结果如下


可以看到这里
相比原始的对象分配,sync.Pool和arena都降低每次操作内存分配-0 allocs/op,前者是复用对象,每次操作没内存申请 0 B/op,后者从arena中分配 8067 B/op
整体来看,相比原始的操作,syncPool每次操作耗时基本不变,但是内存分配大大减少,但是arena虽然内存分配减少,但每次操作耗时增加,可见不是每种场合arena都合适

欢迎关注我@文大侠666,拒绝灌水,只输出对工作有用的技术文章!
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

参考