逃逸分析

5
在许多编程语言中,局部变量都位于调用堆栈中。
在 JavaScript/Python 中,只有闭包变量位于堆中,因为它们必须存在于函数调用之外,才能被创建。
在 GO 中,一些 GO 类型(例如切片类型 []int)确实引用了内存的其他部分,就像 JavaScript/Python 一样。
在 GO 中,并非所有类型的变量都保存引用,就像 Javascript/Python 一样。
例如,
1) [3]int 类型变量 b 直接存储 int 数组,类似于 C,不同的是 C 允许使用 C 语法 &b[index] 访问每个数组元素位置,以获得更多控制。
2) int 类型变量 c 直接存储 int 值,类似于 C,不同的是,C 提供语法(&c)来获取位置访问权限。
在 GO 中,我理解局部变量是否位于堆/栈上取决于应用编译器的逃逸分析,在下面的示例代码中。
func foo() []int {
  // the array lives beyond the call to foo in which it is created
  var a [5]int
  return a[:] // range operator
}

这告诉编译器变量 a 的生命周期超出其作用域,因此在堆上分配内存,而不是在栈上。


问题:

变量 a 是否在堆上分配内存?


1
请注意,并非所有编程语言都像C语言一样。在许多语言(如JavaScript和Lisp)中,闭包实际上是堆栈的一部分。只不过该语言的堆栈并非按照C语言的“堆栈”实现,而是将堆栈实现为链表-这意味着某些语言中的堆栈是在堆中实现的。 JavaScript实现可以实现经典的功能链接列表堆栈,也可以实现某种封闭自由变量的查找机制。它们两者的行为是相同的。 - slebetman
这是一系列关于用Ruby编写Ruby解释器的好文章之一,解释了函数式语言中闭包的传统实现方式:http://hokstad.com/how-to-implement-closures。请注意他的解释:“不要使用传统的栈,而是将函数/方法调用的激活帧(参数和局部变量)作为链表放在堆上”,他正在堆中实现解释器的堆栈。 - slebetman
1个回答

14
在 Go 语言中,你应该相信编译器做出最佳决策。如果可能的话,它会在栈上分配内存。参见FAQ
“从正确性的角度来看,你不需要知道这一点。Go 中的每个变量都存在于引用它的时候。实现选择的存储位置对语言的语义没有影响。”
“存储位置确实会影响编写高效程序。在可能的情况下,Go 编译器将在函数的堆栈帧中分配局部变量。但是,如果编译器无法证明该变量在函数返回后没有被引用,则编译器必须将变量分配到垃圾回收堆上,以避免悬空指针错误。此外,如果局部变量非常大,将其存储在堆上而不是栈上可能更有意义。”
“在当前编译器中,如果一个变量被取地址,那么该变量就是堆分配的候选对象。但是,基本逃逸分析可以识别某些情况,当这些变量在函数返回后不再存在时,它们可以驻留在栈上。”

没有优化(内联),是的 a 将被分配在堆上。我们可以通过传递 -gcflags='-m' 检查逃逸分析(https://play.golang.org/p/l3cZFK5QHO):

$ nl -ba 1.go
     1  package main
     2  
     3  func inlined() []int {
     4      var a [5]int
     5      return a[:]
     6  }
     7  
     8  //go:noinline
     9  func no_inline() []int {
    10      var b [5]int
    11      return b[:]
    12  }
    13  
    14  func main() {
    15      var local_array [5]int
    16      var local_var int
    17      println(no_inline())
    18      println(inlined())
    19      println(local_array[:])
    20      println(&local_var)
    21  }
$ go build -gcflags='-m' 1.go
# command-line-arguments
./1.go:3: can inline inlined
./1.go:18: inlining call to inlined
./1.go:5: a escapes to heap
./1.go:4: moved to heap: a
./1.go:11: b escapes to heap
./1.go:10: moved to heap: b
./1.go:18: main a does not escape
./1.go:19: main local_array does not escape
./1.go:20: main &local_var does not escape

我们可以看到编译器决定将第5行的inlined.a和第10行的no_inline.b分配在堆上,因为它们都逃离了作用域。

然而,在内联后,编译器注意到a不再逃逸,所以它确定该变量可以再次分配在堆栈上(第18行)。

结果是变量a分配在main goroutine的堆栈上,而变量b分配在堆上。从输出中我们可以看到,b的地址是0x1043xxxx,而其它变量的地址均在0x1042xxxx。

$ ./1
[5/5]0x10432020
[5/5]0x10429f58
[5/5]0x10429f44
0x10429f40

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接