逃逸分析
逃逸是什么
当函数内部定义了一个局部变量,然后函数返回这个变量的指针,然后这个变量又在其他地方引用,就发生了逃逸。因为局部变量是在栈上创建的,当函数返回后会销毁
golang的逃逸分析
golang不同于c++,编译器会对局部变量进行分析,然后决定他们是否会分配到栈上还是堆上
- 分配到堆上的开销很大,因为不能像栈一样可以自动清理,只能靠Go进行垃圾回收
- 栈的分配更快,而分配到堆上首先需要去找一片内存块
通过逃逸分析,可以尽量把不需要分配到堆的变量直接分配到栈,堆上的变量少了会减轻堆内存分配的开销,同时减少垃圾回收的压力
怎么完成逃逸分析?
go的逃逸分析原则:如果函数返回了一个变量的引用,就会发生逃逸
逃逸了就放在堆上
1)如果变量在函数外部没有引用,则优先放到栈上
2)如果变量在函数外部存在引用,则必定放到堆上
也就是说一个变量取地址,这个变量也不一定在放堆上,因为他可能没有被引用
怎么确定逃逸?
go build -gcflags '-m -l' xxx.go 编译参数(-gcfl
-gcflags用于启用编译器支持的额外标志
-m 用于输出编译器的优化细节(包括使用逃逸分析这种优化)
-l 用于禁用函数的内联优化,防止逃逸被编译器通过内联彻底的抹除
什么情况发生逃逸
- 编译期间很难确定函数类型,比如如果函数参数为
interface{}
fmt.Println(a ...interface{})
解释:fmt.Println
之类的底层系统函数,实现逻辑会基于interface{}
做反射,通过 reflect.TypeOf(arg).Kind()
获取接口对象的底层数据类型,创建具体类型对象时,会发生内存逃逸。由于 interface{}
的变量,编译时无法确定变量类型以及申请空间大小,所以不能在栈空间上申请内存,需要在 runtime
时动态申请,理所应当地发生内存逃逸。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
-
申请栈空间过大
栈空间大小是有限的,如果编译时发现局部变量申请的空间过大,则会发生内存逃逸,在堆空间上给大变量分配内存
func main() {
num := make([]int, 0, 10000)
_ = num
}
.\main.go:404:13: make([]int, 0, 10000) escapes to heap //发生逃逸
- Slice元素逃逸
type person struct {
Name string
}
func main() {
var num []*person
p1 := &person{
Name: "ss",
}
num = append(num, p1)
}
.\main.go:409:8: &person{...} escapes to heap
未指定slice的len
和cap
时,slice自身未发生逃逸,slice的元素发生逃逸
因为slice会自动扩容,编译器也不知道slice元素大小,扩容后slice的元素可能会被分配到堆空间,因为元素可能不分配到栈上,所以slice整体也不分配到栈上
- 闭包
所谓闭包,就是函数与其所处环境捆绑的组合,也就是说,闭包可以让你在一个内部函数中访问到其外部函数的作用域
go
复制代码func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
in := Increase()
fmt.Println(in()) // 1
fmt.Println(in()) // 2
}
Increase()
返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in
被销毁。很显然,变量 n 占用的内存不能随着函数 Increase()
的退出而回收,因此将会逃逸到堆上。
参考
https://juejin.cn/post/7193607980046041146?searchId=2023091418221995CEB63685D2793789C0