写在前面
在高并发的情况下,如果每次请求都需要申请一块用于计算的内存,比如:
make([]int64, 0, len(ids))
将会是一件成本很高的事情。为了定位项目中的慢语句,我曾经采用“二分法”的方式打印慢日志,定位程序变慢的代码位置。它并不是每次都慢,而是每过几秒钟就突然变得极其慢,TPS能从2000降到200。引起这个问题就是类似于上面这条语句。
slice
make([]int64, 0)
appendsliceslice
a := make([]int64, 0)
fmt.Println(cap(a), len(a))
for i := 0; i < 3; i++ {
a = append(a, 1)
fmt.Println(cap(a), len(a))
}
0 0
1 1
2 2
4 3
每一次扩容空间,都是会重新申请一块区域,把就空间里面的元素复制进来,把新的追加进来。那旧空间里面的元素怎么办?等着垃圾回收呗。
slice
make([]int64, 0, len(ids))
slice
堆还是栈?
程序会从操作系统申请一块内存,而这块内存也会被分成堆和栈。栈可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。
func F() {
temp := make([]int, 0, 20)
...
}
temp
func F() []int{
a := make([]int, 0, 20)
return a
}
而上面这段代码,申请的代码一模一样,但是申请后作为返回值返回了,编译器会认为变量之后还会被使用,当函数返回之后并不会将其内存归还,那么它就会被申请到堆上面了。申请到堆上面的内存才会引起垃圾回收。
那么考考大家,下面这三种情况怎么解释?
func F() {
a := make([]int, 0, 20)
b := make([]int, 0, 20000)
l := 20
c := make([]int, 0, l)
}
abab
ca
可以通过下面的命令查看变量申请的位置。详细内容可以参考我之前的文章《【译】优化Go的模式》
go build -gcflags='-m' . 2>&1
内存碎片化
c := make([]int, 0, l)
临时对象池
syncPool
func (p *Pool) Get() interface{}
Get
很快,我写出了第一版对象池优化方案:
var idsPool = sync.Pool{
New: func() interface{} {
ids := make([]int64, 0, 20000)
return &ids
},
}
func NewIds() []int64 {
ids := idsPool.Get().(*[]int64)
*ids = (*ids)[:0]
idsPool.Put(ids)
return *ids
}
sliceslice
紧接着参考了达达大神的代码sync_pool.go,又写了一版:
var DEFAULT_SYNC_POOL *SyncPool
func NewPool() *SyncPool {
DEFAULT_SYNC_POOL = NewSyncPool(
5,
30000,
2,
)
return DEFAULT_SYNC_POOL
}
func Alloc(size int) []int64 {
return DEFAULT_SYNC_POOL.Alloc(size)
}
func Free(mem []int64) {
DEFAULT_SYNC_POOL.Free(mem)
}
// SyncPool is a sync.Pool base slab allocation memory pool
type SyncPool struct {
classes []sync.Pool
classesSize []int
minSize int
maxSize int
}
func NewSyncPool(minSize, maxSize, factor int) *SyncPool {
n := 0
for chunkSize := minSize; chunkSize <= maxSize; chunkSize *= factor {
n++
}
pool := &SyncPool{
make([]sync.Pool, n),
make([]int, n),
minSize, maxSize,
}
n = 0
for chunkSize := minSize; chunkSize <= maxSize; chunkSize *= factor {
pool.classesSize[n] = chunkSize
pool.classes[n].New = func(size int) func() interface{} {
return func() interface{} {
buf := make([]int64, size)
return &buf
}
}(chunkSize)
n++
}
return pool
}
func (pool *SyncPool) Alloc(size int) []int64 {
if size <= pool.maxSize {
for i := 0; i < len(pool.classesSize); i++ {
if pool.classesSize[i] >= size {
mem := pool.classes[i].Get().(*[]int64)
// return (*mem)[:size]
return (*mem)[:0]
}
}
}
return make([]int64, 0, size)
}
func (pool *SyncPool) Free(mem []int64) {
if size := cap(mem); size <= pool.maxSize {
for i := 0; i < len(pool.classesSize); i++ {
if pool.classesSize[i] >= size {
pool.classes[i].Put(&mem)
return
}
}
}
}
调用例子:
attrFilters := cache.Alloc(len(ids))
defer cache.Free(attrFilters)
Allocslice
DEFAULT_SYNC_POOL = NewSyncPool(
5,
30000,
2,
)
sliceFree
结论
为了优化接口,前前后后搞了一年。结果还是不错的,TPS提升了最少30%,TP99也降低很多。