Go: 了解 Sync.Pool
在项目中的“垃圾回收”中经常会遇到这么一个问题。大量对象被重复分配,导致
GC
的工作量很大。使用sync.Pool
,可以减少分配和GC
工作量。
什么是 sync.Pool
?
Go 1.3 版本的亮点之一是
sync Pool
。它是sync
包下的一个组件,用于创建自我管理的临时检索对象池。
为什么要使用 sync.Pool
?
我们希望尽可能减少GC的开销。 频繁分配和回收内存将给GC造成沉重负担。
sync.Pool
可以缓存暂时不使用的对象,并在下次需要它们时直接使用它们(无需重新分配)。 这样可以潜在地减少GC工作量并提高性能。
如何使用 sync.Pool
?
首先,您需要设一个 新的
function
.。 当池中没有缓存的对象时,将使用此功能。 之后,您只需要使用Get和Put方法来检索和返回对象。 另外,Pool
在首次使用后不得复制。由于新函数类型为
func()interface {}
,因此给方法将返回interface{}
。因此,需要执行类型断言以获取具体对象。
// A dummy struct
type Person struct {
Name string
}
// Initializing pool
var personPool = sync.Pool{
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
New: func() interface{} { return new(Person) },
}
// Main function
func main() {
// Get hold of an instance
newPerson := personPool.Get().(*Person)
// Defer release function
// After that the same instance is
// reusable by another routine
defer personPool.Put(newPerson)
// Using the instance
newPerson.Name = "Jack"
}
Benchmark
type Person struct {
Age int
}
var personPool = sync.Pool{
New: func() interface{} { return new(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)
}
}
}
Benchmark result:
BenchmarkWithoutPool
BenchmarkWithoutPool-8 160698 ns/op 80001 B/op 10000 allocs/op
BenchmarkWithPool
BenchmarkWithPool-8 191163 ns/op 0 B/op 0 allocs/op
Trade-off
Everything* in life is a trade-off.
池也有其性能成本。使用sync.Pool
比简单的初始化要慢得多。
func BenchmarkPool(b *testing.B) {
var p sync.Pool
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
p.Put(1)
p.Get()
}
})
}
func BenchmarkAllocation(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := 0
i = i
}
})
}
Benchmark result:
BenchmarkPool
BenchmarkPool-8 283395016 4.40 ns/op
BenchmarkAllocation
BenchmarkAllocation-8 1000000000 0.344 ns/op
sync.Pool
如何工作?
sync.Pool
has two containers for objects: local pool (active) and victim cache (archived).根据
sync/pool.go
, packageinit
function registers to the runtime as a method 去清理pools
. 此方法将由GC触发。
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
When the GC is triggered, objects inside the victim cache will be collected and then objects inside the local pool will be moved to the victim cache.
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
}
New objects are put in the local pool. Calling
Put
method will put the object into the local pool as well. CallingGet
method will take an object from the victim cache in the first place and if the victim cache was empty the object will be taken from the local pool.
[
sync.Pool localPool and victimCache
For your information, the Go 1.12 sync.Pool implementation uses a
mutex
based locking for thread-safe operations from multiple Goroutines. Go 1.13 introduces a doubly-linked list as a shared pool which removes themutex
lock and improves the shared access.
结论
When there is an expensive object you have to create it frequently, it can be very beneficial to use
sync.Pool
.
PS:
个人英语水平有限,大部分内容觉得原文跟容易理解,翻译之后就变得怪怪的,因此部分内容保留原文。不懂英语不是不想学的借口。