go学习之堆栈中的变量

关于变量分配在堆和栈的问题一直不太了解,直到看了附录几篇文章,有了一个初步的认识。

先看官网怎么说

go变量分配到堆还是栈

从正确的角度来看,你不需要知道。Go中的每个变量只要有引用就会存在。实现选择的存储位置与语言的语义无关。

存储位置对编写高效程序有影响。如果可能的话,Go编译器将为该函数的栈帧(stack frame)中分配本地变量。但是,如果编译器无法证明变量在函数返回后未被引用,则编译器必须在存在垃圾回收的堆(garbage-collected heap)上分配变量以避免指针错误。另外,如果局部变量非常大,将其存储在堆而不是栈上可能更有意义。

在当前的编译器中,如果一个变量的地址被占用,那么该变量就成为堆中的一个候选对象。但是,一些基本的逃逸分析可以识别出某些情况下,这些变量不会在函数的返回后还存在,就可以驻留在栈中。

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

拿slice的一个例子来分析 取自Go中slice作为参数传递的一些“坑”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"unsafe"
)

func main() {
a := []int{}
a = append(a, 7, 8, 9)
fmt.Printf("len: %d cap:%d data:%+v \n", len(a), cap(a), a)
println(&a, 1)
ap(a)
//apRef(&a)
fmt.Printf("len: %d cap:%d data:%+v \n", len(a), cap(a), a)
println(&a, 2)
p := unsafe.Pointer(&a[2])
q := uintptr(p) + 4
t := (*int)(unsafe.Pointer(q))
fmt.Println(*t)
}

func ap(a []int) {
a = append(a, 10)
println(&a, 3)
}

func apRef(a *[]int) {
*a = append(*a, 10)
println(a, 3, cap(*a))
}

运行代码

现学现卖,拿go语言机制简单分析下上边apapRef的区别

  • Go函数是值传递
    ap 去操作 slice a 的时,在 ap 复制一份a
  • Go编译器将常在栈帧中分配本地变量 (高效存储,无需回收下次直接覆盖)
    ap 对内部复制的变量 a 存在自己的 frame stack, 返回后该frame 中的内存状态不确定,因为go可能会在分配给别的函数 frame时覆盖它
  • Go编译器无法证明变量在函数返回后不会被引用 则必须存在堆上(有垃圾回收)
    apRef 参数是指针,操作时,不确定返回后指针是否会被引用,故创建变量到堆上,由指针a指向堆值,main后续访问a值时,是访问的堆上的值

因而ap对a的修改返回后在main里没有体现,停留在未覆盖前的ap frame stack 里;
相反 apRef 对 a指针对应的数据修改保存到堆上,只要有引用就不会被回收,在main中仍可访问

原文还提到一个slice growth的问题,有兴趣可以看下

当然学会怎么分析是最重要的,下边提到的方法要多练习练习
内存分析

  • 编译器报告
    eg: go build -gcflags "-m -m"
    gcflags的参数列表可以使用go tool compile -help查看
  • 测试的压力测试
    eg: go test -run none -bench AlgorithmOne -benchtime 3s -benchmem
  • 测试的性能分析
    eg: go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out

附录

如有疑问,请文末留言交流或邮件:newbvirgil@gmail.com 本文链接 : https://newbmiao.github.io/2018/03/05/go-value-in-heap-and-stack.html