七叶笔记 » golang编程 » go语言变量逃逸分析

go语言变量逃逸分析

简单说一下栈和堆:

  • 只允许往线性表的一端放入数据,之后在这一端取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,类似于一根管子堵住了一端。当我们声明一个函数,函数里面有一些新创建的变量,这些变量就会分配到栈中,而且当当前函数执行结束这些变量也就消失了。
  • 顾名思义,一些杂乱无章的数据都会存进去,就像一个仓库,我们可以放进去很多东西,可以有秩序的放,也可以随意的摆放,当我们需要一个连续的空间但是仓库里却没有足够的连续的空间时,内存分配器会帮我们整理仓库的,然后再给我们一个空间。

go语言也不例外,例如常量等数据都会分配到堆内存中,函数内的变量也会分配到栈中,但是不同的是,go语言中,栈里面的变量有可能跑到堆中,这样的好处是程序员不用关心我们创建一个变量时需要放在堆还是栈中,这个过程就叫做 变量逃逸。 也就是编译器在编译时分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,这个过程就是 变量逃逸分析 。即使程序员使用 Go 语言完成了整个工程后也不会感受到这个过程,这也可以变相的说go语言在一定程度消除了堆和栈的区别。

接下来我们就看一下什么时候会逃逸,首先还是看代码:

package main
​
 import  "fmt"
​
func main() {
 s:= "hello"
 fmt.Println(s)
}
 

这个代码非常简单,我们看一下这里面有没有变量逃逸,我的文件名为: escape .go,所以我使用下面的命令查看一下,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化:

我们看一下结果:

我们可以看到,变量 s 逃逸了,这个的原因是 fmt.Println 的参数为 interface 类型,在编译时不能确定其参数的具体类型,需要在运行时才知道(之前的关于 interface 的文章讲过接口,感兴趣的可以去看一下),所以就会将其分配到堆了。

那么具体在什么时候才会逃逸呢?不急,我们一个一个例子来看。先看一个最经典的例子:

package main
​
type Student struct {
 Name string
 Age int
}
​
func NewStudent(name string, age int) *Student {
 s := new(Student)
 s.Name = name
 s.Age = age
 return s
}
func main() {
 NewStudent("Tom", 18)
}
​
 

我们看一下运行结果:

很显然,new(Student) 逃逸了,这个相信大家应该容易理解,NewStudent 函数中,我们创建一个 Student 类型的指针并将其返回,也就是说这个指针在函数外也用到了,所以也就逃逸了。

我们再看另外一个例子:

package main
​
type Student struct {
 Name string
 Age *int
}
​
func main() {
 var age int
 NewStudent(age)
}
​
func NewStudent(age int) (s Student) {
 s.Age = &age
 return s
}
​
 

显然,又逃逸了。。。原因和上一个类似,age 变量可能在函数外引用,所以也逃逸了。

我们接着往下看:

a := make([]int, 0, 20) // 不会逃逸,因为空间小
b := make([]int, 0, 20000) // 逃逸到堆,空间太大了
c := 20
d := make([]int, 0, c) // 逃逸到堆,因为是一个动态的长度
 

这个我就不演示了,这种数组也会逃逸的。

我们最后说一下 闭包 ,go语言的闭包这里就不细讲了,我们看一下闭包是否会逃逸呢,大家往下看之前可以思考一下。

1

2

3

好,我们继续看代码:

package main
​
import "fmt"
​
// 斐切那波实现
func Fibonacci() func() int {
 a, b := 0, 1
 return func() int {
 a, b = b, a+b
 return a
 }
}
func main() {
 f := Fibonacci()
 for i := 0; i < 10; i++ {
 fmt.Printf("Fibonacci: %d\n", f())
 }
}
 

嗯哼~ 不出所料的逃逸了,Fibonacci()函数中原本属于 局部变量 的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。

我们总结一下:到底什么时候会逃逸呢?

  • 函数返回指针时
  • 栈空间不足,或者说请求空间较大
  • 动态类型,不确定空间大小
  • 闭包

也许还有其他的情况也会逃逸,我这里就不再深究了。我们来看一段官网上的话:

From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.

However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

翻译如下(此断翻译来源于网络):

如何得知变量是分配在栈(stack)上还是堆(heap)上?

准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。 然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数return之后,变量不再被引用,则将其分配到栈上。

我们都知道堆上的内存是会引起GC的,栈中则会随着函数执行结束而消失。GC是比较耗性能的,所以能分配到栈中其实性能也会更好。所以我个人认为,如果我们的代码不引发变量逃逸,性能也许会好那么一丢丢。但是变量逃逸给我们带来的好处要大于这个性能的提升的。两者之间我们敲代码时可以权衡一下。

这篇文章主要是希望大家在编程时可以考虑到这一点,任何事物都有两面性的,没有绝对的应该和不应该,个人认为理解了变量逃逸对敲代码有很大帮助的。

码字不易,如果本文对你有帮助,点赞评论是最大的鼓励(头条对代码的支持不是很好,大家可以去公众号查看文章,公众号:Go语言之美。更多内容会持续在公众号发布)。

相关文章