[go]内存分配/逃逸分析

逃逸分析
逃逸分析就是指程序在编译阶段根据代码中的数据流,对代码中那些变量需要在栈中分配,哪些变量需要在堆上分配进行静态分析的方法,堆栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价较慢,而且会形成内存碎片。栈内存分配则会非常快,栈分配内存只需要两个CPU指令:“PUSH"和RELEASE” 分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。所以逃逸分析才能做到更好的内存分配
逃逸分析的原理

  • pointers to stack objects cannot be stored in the heap: 指向栈对象的指针不能存储到堆中
  • pointers to a stack object cannot outlive that object: 指向栈对象的指针不能超过该对象的存活期,也就说指针不能在栈对象被销毁后依旧存活。(例子: 声明的函数返回并销毁了对象的栈帧,或者它在循环迭代中被重复用于逻辑上不同的变量)

简单来说编译器会根据变量是否被外部引用来决定是否逃逸,逃逸分析是编译器用于决定变量分配变量到堆上还是栈上的一种行为
如果函数外部没有引用,则优先放到栈中
如果函数外部存在引用,则必定放到堆中
何判断变量分配在堆(heap)还是栈(stack)上
根据go里面gc 的逻辑,变量只要被引用就一直会存活是栈上由内部实现决定和具体的语法无关
通常情况下
栈上: 函数调用的参数、返回值以及小类型的局部变量大多分配到栈上,这部分内存由编译器管理,无需GC的标记
堆上: 大对象、逃逸的变量会被分配到堆上,分配到堆上的对象,Go运行时GC会在后台对应的内存进行标记从而能在垃圾回收的时候将对应的内存回收,增加系统开销
栈 可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。下面来看看一个例子

func F() {
	temp := make([]int, 0, 20)
	...
}

上面的例子,内函数内部申请的临时变量,即使你是用make申请到的内存,如果发现在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配比堆上快很多。

func F() []int{
	a := make([]int, 0, 20)
	return a
}

而上面这段代码,申请的代码和上面的一模一样,但是申请后作为返回值返回了,编译器会认为在退出函数之后还有其他地方在引用,当函数返回之后并不会将其内存归还。那么就申请到堆里。
如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销。
指针逃逸
函数传递指针真的比传值效率更高吗
首先,我们知道传递指针可以减少底层值得拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是最高效的
一个典型的变量逃逸案例

package main

type Student struct {
    Name string
    Age  int
}

func StudentRegister(name string, age int) *Student {
    s := new(Student) //局部变量s逃逸到堆

    s.Name = name
    s.Age = age

    return s
}

func main() {
    StudentRegister("Jim", 18)
}

虽然在函数 StudentRegister() 内部 s 为局部变量,其值通过函数返回值返回,s 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。
栈空间不足逃逸 动态类型逃逸
很多函数参数为 interface 类型

func Printf(format string, a ...interface{}) (n int, err error)
func Sprintf(format string, a ...interface{}) string
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Print(a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

编译期间很难确定其参数的具体类型,也能产生逃逸。
逃逸分析的作用
1、逃逸分析首先减少了gc 的压力,不逃逸的对象分配到栈上,当函数返回时就回收了资源,不需要gc 标记清楚
2、 逃逸分析完后可以确定哪些变量分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配而没有发生逃逸的则有编译器在栈上分配)
3、 同步消除,如果用户定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机械码会去掉同步锁
总结
1、 堆上动态分配内存比栈上静态分配内存,开销大很多
2、 变量分配在栈上需要能在编译期间确定它的作用域,否则会被分配到堆上
3、Go编译器会在编译期间对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上,简单来说编译器会根据变量是否被外部引用来决定是否逃逸
4、 对于Go程序员来说,编译器的逃逸分析规则不需要掌握,我们只要通过go build -gcflags '-m’命令来观察变量逃逸情况
5、不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。
6、逃逸分析在编译阶段完成的

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值