golang新特性arena,带你起飞
Posted 文大侠666
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang新特性arena,带你起飞相关的知识,希望对你有一定的参考价值。
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.如果希望内存池被释放后还使用,可拷贝到堆分配空间上
- CloneT any: 克隆一个Arena上对象,只能是指针、slice或者字符串。如果传入的对象不是在Arena分配的,直接原对象返回,否则脱离Arena创建新的对象。
演示代码
func processRequest(req *http.Request)
// 开始创建公共arena内存池
mem := arena.NewArena()
// 最后统一释放内存池
defer mem.Free()
// 分配一系列单对象
for i := 0; i < 10; i++
obj := arena.New[T](mem)
obj.Foo = "Hello"
fmt.Printf("%v\\n", obj)
// 或者分配slice 暂时不支持map
// 参数 mem, length, capacity
slice := arena.MakeSlice[T](mem, 100, 200)
slice[0].Foo = "hello"
fmt.Printf("%v\\n", slice)
// 不能直接分配string,可借助bytes转换
src := "source string"
bs := arena.MakeSlice[byte](mem, len(src), len(src))
copy(bs, src)
str := unsafe.String(&bs[0], len(bs))
fmt.Printf("%v\\n", str)
最佳实践
这里借助Pyroscope的一个优化实践
判断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池中变量
type T struct
Num int
func main()
mem := arena.NewArena()
o := arena.New[T](mem)
mem.Free()
o.Num = 123 // incorrect: use after free
如下操作,可以看到对应的错误
go run -asan main.go
accessed data from freed user arena 0x40c0007ff7f8
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x40c0007ff7f8 pc=0x4603d9]
goroutine 1 [running]:
runtime.throw(0x471778?, 0x404699?)
/go/src/runtime/panic.go:1047 +0x5d fp=0x10c000067ef0 sp=0x10c000067ec0 pc=0x43193d
runtime.sigpanic()
/go/src/runtime/signal_unix.go:851 +0x28a fp=0x10c000067f50 sp=0x10c000067ef0 pc=0x445b8a
main.main()
/workspace/main.go:15 +0x79 fp=0x10c000067f80 sp=0x10c000067f50 pc=0x4603d9
runtime.main()
/go/src/runtime/proc.go:250 +0x207 fp=0x10c000067fe0 sp=0x10c000067f80 pc=0x434227
runtime.goexit()
/go/src/runtime/asm_amd64.s:1598 +0x1 fp=0x10c000067fe8 sp=0x10c000067fe0 pc=0x45c5a1
生产建议
- 不要滥用,和对待unsafe, reflect, or cgo一样,只有必要时用
- 注意释放Free,需要释放后使用的记得Clone
- 实际封装,可以全局封装一个多个持有arena池的单实例对象,或者参考鸟窝大佬的做法,类似context,每个函数传递一个全局分配好的arena池
func bottomUpTreeWithArena(depth int, a *arena.Arena) *Node
...
对比sync.Pool
原理区别
同样都是为了解决频繁分配对象和大量对象GC带来的开销
- sync.Pool
相同类型的对象,使用完后暂时缓存,不GC,下次再有相同的对象分配时直接用之前的缓存的对象,这样避免频繁创建大量对象。
不承诺这些缓存对象的生命周期,GC时会释放之前的缓存,适合解决频繁创建相同对象带来的压力,短时间(两次GC之间)大量创建可能还是会有较大冲击,使用相对简单,但只能用于相同结构创建,不能创建slice等复杂结构
- arena
自己管理内存分配,统一手动释放,对象的生命周期完全自己控制,使用相对复杂,支持slice等复杂结构且可定制性强
性能对比
如下测试,对比测试创建对象的benchmark
type MyObj struct
Index int
func BenchmarkCreateObj(b *testing.B)
b.ReportAllocs()
var p *MyObj
for i := 0; i < b.N; i++
for j := 0; j < 1000; j++
p = new(MyObj)
p.Index = j
var (
objPool = sync.Pool
New: func() interface
return &MyObj
,
)
func BenchmarkCreateObj_SyncPool(b *testing.B)
b.ReportAllocs()
var p *MyObj
for i := 0; i < b.N; i++
for j := 0; j < 1000; j++
p = objPool.Get().(*MyObj)
p.Index = 23
objPool.Put(p)
func BenchmarkCreateObj_Arena(b *testing.B)
b.ReportAllocs()
var p *MyObj
a := arena.NewArena()
defer a.Free()
for i := 0; i < b.N; i++
for j := 0; j < 1000; j++
p = arena.New[MyObj](a)
p.Index = 23
GOEXPERIMENT=arenas go test -bench=. arena_test.go运行,结果如下
可以看到这里
相比原始的对象分配,sync.Pool和arena都降低每次操作内存分配-0 allocs/op,前者是复用对象,每次操作没内存申请 0 B/op,后者从arena中分配 8067 B/op
整体来看,相比原始的操作,syncPool每次操作耗时基本不变,但是内存分配大大减少,但是arena虽然内存分配减少,但每次操作耗时增加,可见不是每种场合arena都合适
参考
代码 https://gitee.com/wenzhou1219/go-in-prod/tree/master/go_120_feature/myarena
- [Golang memory arenas [101 guide]](Golang memory arenas [101 guide])
- Go 1.20 Experiment: Memory Arenas vs Traditional Memory Management
- Go: The Idea Behind Sync.Pool
- 详解 sync.Pool
以上是关于golang新特性arena,带你起飞的主要内容,如果未能解决你的问题,请参考以下文章