最近碰到了在Go
项目中垃圾回收的问题,很多对象大量的重复创建导致GC
的压力很大,而使用sync.pool
能减少重复的对象创建,降低GC
的压力
sync.pool
是什么呢
sync.pool
是Go1.3
发布的一个特性,它是一个临时对象存储池
为什么需要sync.pool
呢
因为项目中频繁的创建对象和回收内存,造成了GC
的压力;而sync.pool
可以缓存对象暂时不用但是之后会用到的对象,并且不需要重新分配内存;这在很大程度上降低了GC
的压力,并且提高了程序的性能
如何使用sync.Pool
呢
首先,你需要为sync.Pool
设置一个New
函数,这个函数就是当你获取不到对象时,返回的默认值。接下来,你就可以通过Get
和Put
方法检索对象和临时存储对象了
注:你创建的这个pool
在第一次使用过后就不能再被赋值了;还有就是Pool
中的对象随时都会被移除,并且不会有通知机制。而如果你存储的是一个对象的引用,那么这个对象也会被回收
package main
import "sync"
type Person struct {
Name string
}
//Initialing pool
var personalPool = sync.Pool{
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
New: func() interface{} {
return &Person{}
},
}
func main() {
// Get hold of an instance
newPerson := personalPool.Get().(*Person)
// Defer release function
// After that the same instance is
// reusable by another routine
defer personalPool.Put(newPerson)
// using instance
newPerson.Name = "Jack"
}
Benchmark
package main
import (
"sync"
"testing"
)
type Person struct {
Age int
}
var (
personPool = sync.Pool{
New: func() interface{} {
return &Person{}
},
}
)
func BenchmarkWithoutPool(b *testing.B) {
var p *Person
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
p = new(Person)
p.Age = 23
}
}
}
func BenchmarkWithPool(b *testing.B) {
var p *Person
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
p = personPool.Get().(*Person)
p.Age = 23
personPool.Put(p)
}
}
}
测试结果:
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkWithoutPool
BenchmarkWithoutPool 7933 146347 ns/op 80000 B/op 10000 allocs/op
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkWithPool
BenchmarkWithPool 1864 595181 ns/op 0 B/op 0 allocs/op
Benchmark 参数说明
结果项 | 含义 |
---|---|
BenchmarkWithoutPool | BenchmarkWithoutPool 是测试的函数名 |
7933 | 表示一共执行了7933次,即b.N的值 |
146347 ns/op | 表示平均每次操作花费了146347纳秒 |
80000B/op | 表示每次操作申请了80000Byte的内存申请 |
10000 allocs/op | 表示每次操作申请了10000次内存 |
当然,这种重复的对象创建是比较合适使用sync.Pool
对象的,但是对于一些简单的初始化动作,就不适合使用sync.Pool
了,因为这也有一定的性能影响,所以需要选择合适的场景使用
sync.Pool
的工作原理
sync.Pool
有两个containers
来存储对象,分别是:local pool
和victim cache
根据sync.pool.go
的源码中可以看出,这个package
的init
函数会注册一个PoolCleanUp
函数,而这个函数就是通过GC
触发的
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
当GC
的触发的时候,在victim
中的对象就会被收集回收,而在local pool
中的对象会被移动victim cache
当中;下面是poolCleanUp
的代码:
func poolCleanup() {
// Drop victim caches from all pools.
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Move primary cache to victim cache.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
新对象是放在local pool
当中的,调用pool.Put
也是将对象放在local pool
当中的。
调用pool.Get
时,会先从victim cache
中获取,如果没有找到,则就会从local pool
中获取,如果local pool
中也没有,就会执行初始化时的New Function
le,否则就返回nil
了
从Go1.12
之后添加了mutex
来保证线程安全;从Go1.13
在sync.pool
中引入了双向链表,移除了mutex
,改善了共享操作
结论
总之,如果有一个对象需要频繁的创建,并且有很大的开销,那么你就可以使用sync.pool
参考:https://medium.com/swlh/go-the-idea-behind-sync-pool-32da5089df72