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++一样,自己管理内存,实际中最容易遇到的问题
- 忘记释放内存,导致OOM
- 引用已经释放的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,拒绝灌水,只输出对工作有用的技术文章!
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!