Closed happyharryh closed 6 months ago
刚看了一篇文章写如何正确使用sync.Pool提升slice的性能,结论是推荐使用指向slice的指针。
sync.Pool
slice
我的理解:使用slice本体版本的sync.Pool(当前gost的使用方式)其实已经可以节省新建slice时为底层数组申请空间和初始化的开销了。但slice的头结构(共24字节)会在被Put时从栈空间逃逸进入堆空间,造成这24字节的内存不可被立即释放或复用,只能等待runtime的下一次自动垃圾回收。而使用指向slice的指针就可以避免这个问题。
runtime
我用三种方案跑了一下Benchmark,的确有差别。目前我对sync.Pool的了解还不是很深入,所以还需大佬确认。
pool_test.go
package pool import ( "sync" "testing" ) //go:noinline func doSomething(buf []byte) { buf[0] = 123 } func BenchmarkRaw(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { buf := make([]byte, 4096) // go优化后不需要每次申请空间,但需要对空间初始化 doSomething(buf) } } // 参考: https://github.com/go-gost/core/blob/master/common/bufpool/pool.go func BenchmarkGost(b *testing.B) { pool := sync.Pool{ New: func() interface{} { buf := make([]byte, 4096) return buf }, } b.ResetTimer() for i := 0; i < b.N; i++ { buf := pool.Get().([]byte) // 每次都需要为buf(`slice`的头结构)申请空间 doSomething(buf) pool.Put(buf) // 底层数组被回收,但`slice`的头结构由栈入堆,不可被复用 } } // 参考: https://blog.mike.norgate.xyz/unlocking-go-slice-performance-navigating-sync-pool-for-enhanced-efficiency-7cb63b0b453e func BenchmarkRecommend(b *testing.B) { pool := sync.Pool{ New: func() interface{} { buf := make([]byte, 4096) return &buf }, } b.ResetTimer() for i := 0; i < b.N; i++ { pBuf := pool.Get().(*[]byte) doSomething(*pBuf) pool.Put(pBuf) } }
测试结果
$ go test -bench . -benchmem goos: darwin goarch: arm64 pkg: pool BenchmarkRaw-8 13387327 89.52 ns/op 0 B/op 0 allocs/op BenchmarkGost-8 42734725 27.74 ns/op 24 B/op 1 allocs/op BenchmarkRecommend-8 139212199 8.516 ns/op 0 B/op 0 allocs/op PASS ok pool 5.762s
是的,确实不是最优的 目前最高性能的是 sing 13ns/op
之前的版本中采用的是slice指针方式,但从代码的可读性和使用上的便利性方面考虑最终放弃了。
刚看了一篇文章写如何正确使用
sync.Pool
提升slice
的性能,结论是推荐使用指向slice
的指针。我的理解:使用
slice
本体版本的sync.Pool
(当前gost的使用方式)其实已经可以节省新建slice
时为底层数组申请空间和初始化的开销了。但slice
的头结构(共24字节)会在被Put时从栈空间逃逸进入堆空间,造成这24字节的内存不可被立即释放或复用,只能等待runtime
的下一次自动垃圾回收。而使用指向slice
的指针就可以避免这个问题。我用三种方案跑了一下Benchmark,的确有差别。目前我对
sync.Pool
的了解还不是很深入,所以还需大佬确认。pool_test.go
测试结果