内存分配问题
Slice扩充机制
初始化一个slice,初学者会用:
make([]int64, 0)
高级一些的程序员都会知道,这样第一次分配内存相当于没有分配,如果要后续append元素,会引起slice以指数形式扩充,可以参考下面的代码,追加了3个元素,slice扩容了3次。
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多次扩容申请内存,但还是有问题的。
程序的内存分配、栈内存和堆内存
程序的内存分为以下几个部分:
1、栈区(stack)—由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
2、堆区(heap)— 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。-程序结束后有系统释放
4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
如下定义的临时变量,将分配到栈区,函数执行之后会自动释放。
func F() {
temp := make([]int, 0, 20)
...
}
好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。
如下定义的三个变量,都将分配到堆区。
func F() []int{
a := make([]int, 0, 20)
b := make([]int, 0, 20000)
l := 20
c := make([]int, 0, l)
return a
}
变量a因为是返回值,系统默认其还将在接下来的程序中起作用,因此不分配到栈区。
变量b虽然是临时变量,但申请的内存很大,也将分配到堆区。
变量c因为分配的长度不定,也将分配到堆内存。
内存碎片化
实际项目基本都是通过 c := make([]int, 0, l) 来申请内存,长度都是不确定的。自然而然这些变量都会申请到堆上面了。Golang使用的垃圾回收算法是『标记——清除』。简单得说,就是程序要从操作系统申请一块比较大的内存,内存分成小块,通过链表链接。每次程序申请内存,就从链表上面遍历每一小块,找到符合的就返回其地址,没有合适的就从操作系统再申请。如果申请内存次数较多,而且申请的大小不固定,就会引起内存碎片化的问题。申请的堆内存并没有用完,但是用户申请的内存的时候却没有合适的空间提供。这样会遍历整个链表,还会继续向操作系统申请内存,申请一块内存变成了慢语句。
slice长度不定—> 分配到堆内存 —> 分页式内存分配导致内存碎片化 —> 申请内存语句速度急剧下降
用临时对象池构建本地缓存
什么是临时对象池
sync.Pool — 临时对象池是一些可以分别存储和取出的临时对象。池中的对象会在没有任何通知的情况下被移出。实际上,这个清理过程是在每次垃圾回收之前做的。垃圾回收是固定两分钟触发一次。而且每次清理会将Pool中的所有对象都清理掉!
Pool 结构体的定义为:
type Pool struct {
noCopy noCopy
local unsafe.Pointer // 本地P缓存池指针
localSize uintptr // 本地P缓存池大小
// 当池中没有可能对象时
// 会调用 New 函数构造构造一个对象
New func() interface{}
}
Pool 中有两个定义的公共方法,分别是 Put - 向池中添加元素;Get - 从池中获取元素,如果没有,则调用 New 生成元素,如果 New 未设置,则返回 nil。
临时对象池是协程安全的。
临时对象池的使用
代码实现:
package main
import (
"fmt"
"sync"
"time"
)
// 一个[]byte的对象池,每个对象为一个[]byte
var bytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, 1024)
return &b
},
}
func main() {
a := time.Now().Unix()
// 不使用对象池
for i := 0; i < 1000000000; i++ {
obj := make([]byte, 1024)
_ = obj
}
b := time.Now().Unix()
// 使用对象池
for i := 0; i < 1000000000; i++ {
obj := bytePool.Get().(*[]byte)
bytePool.Put(obj)
}
c := time.Now().Unix()
fmt.Println("without pool ", b-a, "s")
fmt.Println("with pool ", c-b, "s")
}
输出结果:
without pool 20 s
with pool 15 s
临时对象池的局限性
这里的例子来自 https://cyent.github.io/golang/goroutine/sync_pool/ , 感谢原博主!
只有当每个对象占用内存较大时候,用pool才会改善性能。
- 对比1:
package main
import (
"fmt"
"sync"
"time"
)
// 一个[]byte的对象池,每个对象为一个[]byte
var bytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, 1)
return &b
},
}
func main() {
a := time.Now().Unix()
// 不使用对象池
for i := 0; i < 1000000000; i++ {
obj := make([]byte, 1)
_ = obj
}
b := time.Now().Unix()
// 使用对象池
for i := 0; i < 1000000000; i++ {
obj := bytePool.Get().(*[]byte)
bytePool.Put(obj)
}
c := time.Now().Unix()
fmt.Println("without pool ", b-a, "s")
fmt.Println("with pool ", c-b, "s")
}
输出
without pool 0 s
with pool 17 s
可以看到,当[]byte只有1个元素时候,用pool性能反而更差
- 对比2:
package main
import (
"fmt"
"sync"
"time"
)
// 一个[]byte的对象池,每个对象为一个[]byte
var bytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, 800)
return &b
},
}
func main() {
a := time.Now().Unix()
// 不使用对象池
for i := 0; i < 1000000000; i++ {
obj := make([]byte, 800)
_ = obj
}
b := time.Now().Unix()
// 使用对象池
for i := 0; i < 1000000000; i++ {
obj := bytePool.Get().(*[]byte)
bytePool.Put(obj)
}
c := time.Now().Unix()
fmt.Println("without pool ", b-a, "s")
fmt.Println("with pool ", c-b, "s")
}
输出
without pool 16 s
with pool 17 s
这时,是否使用临时内存池,性能差别不大。
- 对比3:
package main
import (
"fmt"
"sync"
"time"
)
// 一个[]byte的对象池,每个对象为一个[]byte
var bytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, 8000)
return &b
},
}
func main() {
a := time.Now().Unix()
// 不使用对象池
for i := 0; i < 1000000000; i++ {
obj := make([]byte, 8000)
_ = obj
}
b := time.Now().Unix()
// 使用对象池
for i := 0; i < 1000000000; i++ {
obj := bytePool.Get().(*[]byte)
bytePool.Put(obj)
}
c := time.Now().Unix()
fmt.Println("without pool ", b-a, "s")
fmt.Println("with pool ", c-b, "s")
}
输出
without pool 128 s
with pool 17 s
临时内存池终于发挥了作用!
总结:pool适合占用内存大且并发量大的场景。当内存小并发量少的时候,使用pool适得其反