变量逃逸(Escape Analysis)
概要
堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在C/C++语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈;全局变量、结构体成员使用堆分配等。程序员不得不花费很多年
的时间在不同的项目中学习、记忆这些概念并加以实践和使用。Go语言将这个过程整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,即使程序员使用Go语言完成了整个工程后也不会感受到这个过程。
逃逸分析
` 试用下面的代码来展现Go语言如何通过命令行分析变量逃逸,代码如下
package main
import "fmt"
//该函数测试入口参数以及返回值情况
//dummy()函数拥有一个参数,返回一个整型值,测试函数参数和返回值分析情况
func dummy (b int) int {
//声明一个c赋值进入参数并返回 这里演示函数临时变量通过函数返回值返回后的情况
var c int
c = b
return c
}
//空函数 测试没有任何参数函数的分析情况
func void() {
}
func main() {
//声明a变量并打印 测试main()中变量的分析情况
var a int
//调用void()函数 没有返回值 测试void()调用后的分析情况
void()
//打印a变量的值和dummy()函数返回 测试函数返回值没有变量接收时的分析情况
fmt.Println(a, dummy(0))
}
接着使用如下命令行运行上面的代码:
$ go run -gcflags "-m -1" main.go
使用go run运行程序时,-gcflags参数是编译参数。其中-m表示进行内存分析,-1表示避免程序内联,也就是避免进行程序优化。
程序运行结果如下:
# command-line-arguments
./main.go:29:13: a escapes to heap
./main.go:29:22: dummy(0) escapes to heap
./main.go:29:13: main ... arguments does not escape
0 0
程序运行结果分析
输出第2行告知“main 的第29行的变量a逃逸到堆”
第3行告知“dummy(0)调用逃逸到堆”。由于 dummyO)函数会返回一个整型值,这个值被fmt.Println使用后还是会在其声明后继续在main()函数中存在。
第4行,这句提示是默认的,可以忽略。 上面例子中,变量c为整形,其值通过dummy()的返回值"逃出"了dummy()函数。c变量值被复制,且被作为dummy()函数返回值返回,即使c变量在dummy()函数中分配的内存被释放,也不会影响main()中dummy()返回的值。c变量使用栈分配不会影响结果
取地址发生逃逸
下面的例子使用结构体做数据,了解在堆上分配的情况
package main
import "fmt"
//声明空结构体测试结构体逃逸情况
type Data struct {
}
//将dummy()函数的返回值修改为*Data指针类型
func dummy() *Data {
//实例化c为Data类型,此时c的结构体为值类型
var c Data
//返回函数局部变量地址
return &c
}
func main() {
//打印dummy()函数的返回值
fmt.Println(dummy())
}
执行逃逸分析:
$ go run -gcflags "-m -1" main.go
#command-line-arguments
./main.go:15:9 &c escapes to heap
./main.go:12:6 moved to heap: c
./main.go:20:19 dummy() escapes to heap
./main.go:20:13 main ... arguments does not escape
&{}
注意第4行出现了新的提示:将c移到堆中。这句话表示,Go编译器已经确认如果将c变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,dummyO)的返回值将是
Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++语言中容易犯错的地方:引用了一个函数局部变量的地址。
Go语言最终选择将c的Data结构分配在堆上。然后由垃圾回收器去回收c的内存。
原则
在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在Java 等语言的编译器优化上也使用了类似的技术。
编译器觉得变量应该分配在堆和栈上的原则是:
- 变量是否被取地址。
- 变量是否发生逃逸。