逃逸分析,简单来说Go是通过编译器里面做逃逸分析(escape analysis)来决定一个对象是放栈上,还是堆上,不逃逸的对象放在栈上,可能逃逸的对象放在堆上。栈上的内存分配和回收比堆上快的多

堆和栈

  • 堆 手动申请、分配、释放内存等。堆适合不可预知大小的内存分配,付出的代价就是分配速度慢,回收时会形成内存碎片
  • 栈 由编译器进行管理,自动申请、分配、释放。一般不会太大,因此栈分配和回收速度非常快,函数参数、局部变量等会存放到栈上

为何需要逃逸分析

  • 减少gc压力,栈上的变量随着函数退出后系统自动回收,不需要gc标记后清除
  • 减少内存碎片的产生
  • 减少分配堆内存的开销,提高程序的运行速度

什么阶段确定逃逸

编译阶段确定逃逸,注意不是在运行时,因为编译阶段已经确定对象存放到栈内存还是堆内存

如何逃逸分析

// 通过编译器命令查看
go run -gcflags '-m -l' main.go

// 通过反编译命令查看
go tool compile -S main.go
  • -m打印出逃逸分析的优化策略
  • -l禁用函数内联,减少干扰

可能出现逃逸

  • 对一个变量取地址(不取地址的话,局部变量比较小就分配到栈上),可能会分配到堆上,但是编译器进行逃逸分析后,如果发现函数返回后此变量不会被引用,那么还是会分配到栈上。
  • 未确定类型interface{},不确定长度大小,编译器在编译的时候很难知道函数的调用或者结构体的赋值过程会是什么类型,因此只能分配到堆上
  • 栈空间不足,创建一个长度为10w的切片,就会逃逸到堆上
  • 间接赋值,对某个引用对象中的引用类型赋值。(引用类型:func, interface, slice, map, chan, *Type指针)
  • 闭包引用对象逃逸,闭包内的变量原本是局部变量,由于闭包的引用不得不放在堆上

提问:函数传递指针真的比传值效率高吗?

我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

总结

  • 静态分配到栈上,性能一定比动态分配到堆上好
  • 底层分配到堆还是栈,实际上对你来说是透明的,不需要过度关心
  • 每个Go版本的逃逸分析会有所不同(会改变,会优化)
  • 到处都有指针传递并不一定是对的,要用对。

参考

Go逃逸分析

我要在栈上。不,你应该在堆上

内存逃逸分析