Go语言之变量逃逸(Escape Analysis)分析

前面已经详细分析过堆和栈的区别,变量是如何分配在堆和栈上的,go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。
但有时我们希望函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配等。

那么到底该如何分配呢?
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()    

    // 打印a变量的值和dummy()函数返回    
    fmt.Println(a, dummy(0))
}

解析:

  • dummy() 函数拥有一个参数,返回一个整型值,用来测试函数参数和返回值分析情况。
  • 声明变量 c,用于演示函数临时变量通过函数返回值返回后的情况。
  • func void():这是一个空函数,测试没有任何参数函数的分析情况。
  • 在 main() 中声明变量 a,测试 main() 中变量的分析情况。
  • 调用 void() 函数,没有返回值,测试 void() 调用后的分析情况。
  • fmt.Println(a, dummy(0)):打印 a 和 dummy(0) 的返回值,测试函数返回值没有变量接收时的分析情况。

接着使用如下命令行运行上面的代码:

go run -gcflags "-m -l" main.go

解析:

使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。

运行结果如下:

# 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 ... argument does not escape

0 0

程序运行结果分析如下:

  • 第 2 行告知“代码的第 29 行的变量 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())
 }

解析:

  • type Data struct:声明一个空的结构体做结构体逃逸分析。
  • func dummy() *Data:将 dummy() 函数的返回值修改为 *Data 指针类型。
  • var c Data:将变量 c 声明为 Data 类型,此时 c 的结构体为值类型。
  • return &c:取函数局部变量 c 的地址并返回。
  • fmt.Println(dummy()):打印 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语言,在 Java 等语言的编译器优化上也使用了类似的技术。

编译器觉得变量应该分配在堆和栈上的原则是:

  • 变量是否被取地址
  • 变量是否发生逃逸
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值