golang中变量的逃逸分析

go中逃逸分析是怎么进行的

  1. 变量逃逸的基本原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸
  2. 简单来说编译器会分析代码的特征和代码的生命周期,go中的变量只有在编译器可以证明函数返回后不会再被引用的,
    才会被分配到栈上,其它情况都分配到堆上
  3. go语言中没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量
    分配到何处
  4. 对一个变量取地址,可能会被分配到堆上,但是编译器经过逃逸分析后,如果发现函数返回后,此变量不会再被引用
    那么还是会被分配到栈上
  5. 编译器通过变量是否会被外部引用来决定是否逃逸
    • 如果函数外部没有引用,则优先放入栈中
    • 如果函数外部存在引用,则必定放入堆中
  6. 变量在栈上存储时,函数结束后变量就销毁了,变量在堆上存储时,函数结束后变量不会被销毁
  7. 逃逸分析这种操作把变量合理的分配到了它该去的地方,即使是使用new申请的内存,如果发现在退出函数时没有用了
    就把你丢到栈上,毕竟栈上的内存分配比堆上要快很多,反之,即使你表面上是一个普通的变量,但是经过逃逸分析后发现
    退出函数时还有其它地方引用,那么就把它分配到堆上
  8. 如果把变量都分配到堆上,堆不像栈可以自动的清理,这回引起go频繁的进行垃圾回收,而垃圾回收占用比较大的系统开销
  9. 堆和栈相比,堆适合不可预知大小的内存分配,但是代价是堆内存分配较慢,而且会形成内存碎片,栈内存分配则会非常快
    只需要两个指令,push release即可分配和释放,而堆分配内存首先需要去找到一块大小何时的内存块,之后通过垃圾回收才能释放
  10. 通过逃逸分析可以尽量把那些不需要分配到堆上的变量直接分配带栈上,堆上的变量变少了,可以减小堆内存分配的开销
    同时减轻gc进行垃圾回收的压力,提高程序的运行速度
    【引申1】如何查看某个变量是否发生了逃逸?
    两种方法:使用go命令,查看逃逸分析结果;反汇编源码;比如用这个例子:
func foo() *int {
	t := 3
	return &t
}

func main() {
	x := foo()
	fmt.Println(*x)
}

使用命令:go build -gcflags '-m -l' .\server\main.go
加-l是为了不让foo函数被内联。得到如下输出:

# command-line-arguments
server\main.go:6:2: moved to heap: t
server\main.go:12:13: ... argument does not escape
server\main.go:12:14: *x escapes to heap

foo函数里的变量t逃逸了,和我们预想的一样,但是为什么main函数中的*x也逃逸了,因为有些函数参数是interface类型
编译期间很难确定它的类型,也会发生逃逸
【引申2】下面代码发生逃逸了吗,为什么

type S struct {}

func main() {
	var s S
	_ = identity(s)
}

func identity(x S) S{
	return x
}

没有发生逃逸,因为go语言的函数传参都是值传递,调用函数的时候,直接在栈上copy出一份参数,不存在逃逸

type S struct {}

func main() {
	var x S
	y := &x
	_ = *identity(y)
}

func identity(z *S) *S{
	return z
}

identity函数的输入直接当成返回值了,因为没有对其做引用,所以z没有逃逸,对x的引用也没有逃出main函数的作用域,所以x也没有逃逸

type S struct {}

func main() {
	var x S
	_ = *identity(x)
}

func identity(z S) *S{
	return &z
}

分析,z是对x的拷贝,identity函数中对z取了引用,所以z不能放到栈上,否则在identity函数之外,通过引用如何找到z,
所以z必须逃逸到堆上,尽管在main函数中直接丢弃了identity返回的结果,但是go编译器还没有那么只能,分析不出这种情况
而对x从来没有取引用,所以x不会发生逃逸
【引申4】如果对一个结构体成员赋引用如何

type S struct {
	M *int
}

func main() {
	var x = 5
	_ = refStruct(x)
}

func refStruct(y int) (z S) {
	z.M = &y
	return z
}

refStruct函数对y取了引用,所以y发生了逃逸
【示例5】

type S struct {
	M *int
}

func main() {
	var x = 5
	_ = refStruct(&x)
}

func refStruct(y *int) (z S) {
	z.M = y
	return z
}

分析:在main函数里对i取了引用,并且把它传给了refStruct函数,i的引用一直在main函数的作用域用,
因此i没有发生逃逸。和上一个例子相比,有一点小差别,但是导致的程序效果是不同的:例子4中,i先在main的栈帧中分配,
之后又在refStruct栈帧中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通过引用传递。
【示例6】

type S struct {
	M *int
}

func main() {
	var x = 5
	var s S
	refStruct(&x, &s)
}

func refStruct(y *int, z *S) {
	z.M = y
}

分析:本例i发生了逃逸,按照前面例子5的分析,i不会逃逸。两个例子的区别是例子5中的S是在返回值里的,
输入只能“流入”到输出,本例中的S是在输入参数中,所以逃逸分析失败,i要逃逸到堆上。

参考文档

posted @ 2022-03-16 13:51  专职  阅读(235)  评论(0编辑  收藏  举报