Go变量逃逸分析

栈(Stack)是一种拥有特殊规则的线性表数据结构
1) 概念
栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序。往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。
从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的元素数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除了栈顶部的成员)进行任何查看和修改操作
栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。
变量和栈有什么关系
栈可用于内存分配,栈的分配和回收速度非常快;
func calc(a, b int) int { var c int c = a * b var x int x = c * 10 return x }
Go语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。
堆通常是一个可以被看做一棵完全二叉树的数组对象。妈的不了解二叉树的还以为是个啥呢。举栗说明是最常见的手法:
堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小,分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往这个空间里摆放家具会发现虽然有足够的空间,但各个空间分布在不同的区域,没有一段连续的空间来摆放家具
堆栈相比
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
变量逃逸分析(Escape Anlysis)自动决定变量分配方式,提高运行效率
在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配等。程序员不得不花费很长的时间在不同的项目中学习、记忆这些概念并加以实践和使用。
Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配
package main import "fmt" // 本函数测试入口参数和返回值情况 func dummy(b int) int { // 声明一个变量c并赋值 var c int c = b return c } // 空函数, 什么也不做 func void() { } func main() { // 声明a变量并打印 var a int // 调用void()函数 void() // 打印a变量的值和dummy()函数返回 fmt.Println(a, dummy(0)) }
执行命令
go run -gcflags "-m -l" main.go
使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化
结果
go run -gcflags "-m -l" escape.go
# command-line-arguments
./escape.go:28:13: ... argument does not escape
./escape.go:28:13: a escapes to heap 变量a逃逸到堆
./escape.go:28:22: dummy(0) escapes to heap
//由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在 main() 函数中继续存在。
0 0
上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。变量 c 的值被复制并作为 dummy() 函数的返回值返回,即使变量 c 在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。变量 c 使用栈分配不会影响结果。
2) 取地址发生逃逸
下面的例子使用结构体做数据,来了解结构体在堆上的分配情况,代码如下:
  1. package main
  2. import "fmt"
  3. // 声明空结构体测试结构体逃逸情况
  4. type Data struct {
  5. }
  6. func dummy() *Data {
  7. // 实例化c为Data类型
  8. var c Data
  9. //返回函数局部变量地址
  10. return &c
  11. }
  12. func main() {
  13. fmt.Println(dummy())
  14. }
代码说明如下:
  • 第 6 行,声明一个空的结构体做结构体逃逸分析。
  • 第 9 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
  • 第 11 行,将变量 c 声明为 Data 类型,此时 c 的结构体为值类型。
  • 第 14 行,取函数局部变量 c 的地址并返回。
  • 第 18 行,打印 dummy() 函数的返回值。
执行逃逸分析:
go run -gcflags "-m -l" 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 ... argument does not escape
&{}
注意第 4 行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将变量 c 分配在栈上是无法保证程序最终结果的,如果这样做,dummy() 函数的返回值将是一个不可预知的内存地址,这种情况一般是 C/C++ 语言中容易犯错的地方,引用了一个函数局部变量的地址。
Go语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。
3) 原则
在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆的问题上,编译器会自动帮助开发者完成这个纠结的选择,但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在 Java 等语言的编译器优化上也使用了类似的技术。
编译器觉得变量应该分配在堆和栈上的原则是:
  • 变量是否被取地址;
  • 变量是否发生逃逸。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注