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的方法,后续可能会加上。

  • NewT any *T: 创建一个对象
  • MakeSliceT any []T: 在Arena创建一个Slice。

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++一样,自己管理内存,实际中最容易遇到的问题

  1. 忘记释放内存,导致OOM
  2. 引用已经释放的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新特性arena,带你起飞的主要内容,如果未能解决你的问题,请参考以下文章

带你快速浏览Xcode 9新特性

ES6新特性

Java 17 新特性,快到起飞。。惊呆了!

Java 17新特性,快到起飞?惊呆了!

Java 17新特性,快到起飞?惊呆了!

Java 17新特性,快到起飞?惊呆了!