Go语言之GO 语言变量逃逸分析
GO 语言变量逃逸分析
栈
栈只允许从线性表的同一端放入和取出数据,按照后进先出(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 Analysis)
——自动决定变量分配方式,提高运行效率
在C语言中需要手动进行内存分配,例如局部变量可以分配栈,全局变量或者结构体可以分配堆。
Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。
逃逸分析
通过下面的代码来展现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()
fmt.Println(a, dummy(0))
}
-------------------------------------------
dummy() 函数拥有一个参数,返回一个整型值,用来测试函数参数和返回值分析情况。
声明变量 c,用于演示函数临时变量通过函数返回值返回后的情况。
这是一个空函数,测试没有任何参数函数的分析情况。
在 main() 中声明变量 a,测试 main() 中变量的分析情况。
调用 void() 函数,没有返回值,测试 void() 调用后的分析情况。
打印 a 和 dummy(0) 的返回值,测试函数返回值没有变量接收时的分析情况。
go run -gcflags "-m -l" main.go
使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。
PS D:\goprogram\go\src\day05> go run -gcflags "-m -l" .\lianxi.go
# command-line-arguments
.\lianxi.go:21:13: main ... argument does not escape
.\lianxi.go:21:13: a escapes to heap
.\lianxi.go:21:22: dummy(0) escapes to heap
0 0
运行结果分析
第 2 行告知“代码的第 13 行的变量 a 逃逸到堆”。
第 3 行告知“dummy(0) 调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在 main() 函数中继续存在。
第 4 行,这句提示是默认的,可以忽略。
上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。变量 c 的值被复制并作为 dummy() 函数的返回值返回,即使变量 c 在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。变量 c 使用栈分配不会影响结果。
取地址发生逃逸
package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct {
}
func dummy() *Data {
// 实例化c为Data类型
var c Data
//返回函数局部变量地址
return &c
}
func main() {
fmt.Println(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 的内存。
原则
在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆的问题上,编译器会自动帮助开发者完成这个纠结的选择,但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在 Java编译器觉得变量应该分配在堆和栈上的原则是:
- 变量是否被取地址;
- 变量是否发生逃逸。