内存分配问题

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适得其反