是否复用buffer?
场景:压缩/解压缩场景,需要buffer来缓冲压缩或解压缩数据,eg:
go
func main() {
var buf = make([]byte, 0, 1024)
out, err := gzipCompress([]byte("hello world"), buf)
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Println(out)
}
func gzipCompress(src []byte, buf []byte) (out []byte, err error) {
buffer := bytes.NewBuffer(buf)
writer := gzip.NewWriter(buffer)
_, err = writer.Write(src)
if err != nil {
writer.Close()
fmt.Println("err1:", err)
return nil, err
}
err = writer.Close()
if err != nil {
fmt.Println("err2:", err)
return nil, err
}
return buffer.Bytes(), nil
// ====另一种返回数据的方式。(埋个坑,后续讲)====
//out = make([]byte, len(buffer.Bytes()))
//copy(out, buffer.Bytes())
//return out, nil
}
每次调用gzipCompress
时都要一个缓冲区,有2种选择:
- 每次都新建,但频繁新建对象开销会比较大
- 使用
sync.Pool
创建缓冲池,复用之前新建的buffer,减少频繁新建对象的开销。
那到底是复用buffer好,还是每次都新建buffer好呢?
先说结论:
复用切片是有坑的:复用buffer的话,压缩后返回数据最好copy一份新的,避免复用带来的坑:下次压缩再复用buffer时可能把上次的返回结果给篡改(后续「复用buffer的坑」会讲)。
但copy数据的话就要out = make([]byte, len(buffer))
再copy(out, buffer)
,和不复用buffer一样都要频繁创建buffer,反而不复用buffer时还不会copy数据,所以是不是不复用更好?其实也不一定,复用的话,copy时make([]byte, len(buffer))
容量是提前定好的,不复用的话,除非每次新建的buffer容量一定足够、开销才会比复用低,否则就会扩容,而扩容的开销很大:至少会扩容一次,创建一次容量足够的数组,然后copy数据,也会包含复用buffer时「out = make([]byte, len(buffer))
再copy(out, buffer)
的开销」。
复用buffer的坑
go
func main() {
test1()
}
func test1() {
var b = make([]byte, 0, 1024)
b = b[:0]
out := compress([]byte("hello"), b)
fmt.Println("out:", string(out))
b = b[:0]
out2 := compress([]byte("world"), b)
fmt.Println("out:", string(out))
fmt.Println("out2:", string(out2))
}
func compress(data []byte, b []byte) (out []byte) {
buf := bytes.NewBuffer(b)
buf.Write(data)
buf.Write([]byte("compress_tag"))
// ====返回数据的方案1,copy一份新数据===
out = make([]byte, len(buf.Bytes()))
copy(out, buf.Bytes())
return out
// ====返回数据的方案2===
//return buf.Bytes()
}
compress
方法采用方案1(copy一份新数据)返回数据的执行结果:
text
out: hellocompress_tag
out: hellocompress_tag
out2: worldcompress_tag
compress
方法采用方案2返回数据的执行结果:
text
out: hellocompress_tag
out: worldcompress_tag
out2: worldcompress_tag
可以看到,方案2的话,第2次compress
会影响第1次compress
的返回结果。
slice底层原理
分级缓冲池
固定缓冲池:
go
var pool = sync.Pool{
New: func() any {
return make([]byte, 0, 1024)
},
}
分级缓冲池,更精细化控制:
go
type MultiLevelBufPool struct {
small sync.Pool
medium sync.Pool
large sync.Pool
}
func NewMultiLevelBufPool() *MultiLevelBufPool {
return &MultiLevelBufPool{
small: sync.Pool{
New: func() any {
return make([]byte, 0, 1024) // 1KB
},
},
medium: sync.Pool{
New: func() any {
return make([]byte, 0, 4096) // 4KB
},
},
large: sync.Pool{
New: func() any {
return make([]byte, 0, 16<<10) // 16KB
},
},
}
}
func (m *MultiLevelBufPool) GetBuffer(expectedSize int) []byte {
switch {
case expectedSize <= 1024:
return m.small.Get().([]byte)[:0]
case expectedSize <= 4096:
return m.medium.Get().([]byte)[:0]
default:
return m.large.Get().([]byte)[:0]
}
}
func (m *MultiLevelBufPool) PutBuffer(buf []byte) {
switch {
case len(buf) <= 1024:
m.small.Put(buf[:0])
case len(buf) <= 4096:
m.medium.Put(buf[:0])
case len(buf) <= 16<<10:
m.large.Put(buf[:0])
default:
// 丢弃
return
}
}