1 sync.Pool 的使用场景
一句话总结:保存和复用临时对象,减少内存分配,降低 GC 压力。
json 的反序列化在文本解析和网络通信过程中非常常见,当程序并发度非常高的情况下,短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。
Go 语言从 1.3 版本开始提供了对象重用的机制,即 sync.Pool。sync.Pool 是可伸缩的,同时也是并发安全的,其大小仅受限于内存的大小。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。这样就可以不用再次经过内存分配,可直接复用已有对象,减轻 GC 的压力,从而提升系统的性能。
sync.Pool 的大小是可伸缩的,高负载时会动态扩容,存放在池中的对象如果不活跃了会被自动清理。
2 如何使用
2.1 声明对象池
只需要实现 New 函数即可。对象池中没有对象时,将会调用 New 函数创建。
var studentPool = sync.Pool{
New: func() interface{} {
return new(Student)
},
}
2.2 Get & Put
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
Get()
用于从对象池中获取对象,因为返回值是interface{}
,因此需要类型转换。Put()
则是在对象使用完毕后,返回对象池。
3 性能测试
3.1 struct 反序列化
func BenchmarkUnmarshal(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := &Student{}
json.Unmarshal(buf, stu)
}
}
func BenchmarkUnmarshalWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
}
}
测试结果如下:
$ go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: example/hpg-sync-pool
BenchmarkUnmarshal-8 1993 559768 ns/op 5096 B/op 7 allocs/op
BenchmarkUnmarshalWithPool-8 1976 550223 ns/op 234 B/op 6 allocs/op
PASS
ok example/hpg-sync-pool 2.334s
在这个例子中,因为 Student 结构体内存占用较小,内存分配几乎不耗时间。而标准库 json 反序列化时利用了反射,效率是比较低的,占据了大部分时间,因此两种方式最终的执行时间几乎没什么变化。但是内存占用差了一个数量级,使用了 sync.Pool
后,内存占用仅为未使用的 234/5096 = 1/22,对 GC 的影响就很大了。
3.2 bytes.Buffer
var bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
var data = make([]byte, 10000)
func BenchmarkBufferWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Write(data)
buf.Reset()
bufferPool.Put(buf)
}
}
func BenchmarkBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
var buf bytes.Buffer
buf.Write(data)
}
}
这个例子创建了一个 bytes.Buffer
对象池,而且每次只执行一个简单的 Write
操作,存粹的内存搬运工,耗时几乎可以忽略。而内存分配和回收的耗时占比较多,因此对程序整体的性能影响更大。