面试准备-编译

编译

逃逸分析怎么进行

逃逸分析是编译器在执行代码分之后,为了对内存分配进行简化和优化,去决定一个变量是分配到栈上还是分配到堆上。

C和C++中调用malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。所以很容易发生内存泄露。但是在Go语言中,因为GC的原因,我们基本不用担心内存泄露的问题。虽然也有new函数,但是使用new函数得到的内存不一定就在堆上。

Go语言逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。与C和C++不同的是Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。

逃逸分析这种“骚操作”把变量合理地分配到它该去的地方。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。

通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。

编译器会根据变量是否被外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;

逃逸分析原则

  • 编译阶段无法确定的参数,会逃逸到堆上;
  • 变量在函数外部存在引用,会逃逸到堆上;不存在引用,则会继续在栈上;
  • 变量占用内存较大时,会逃逸到堆上;

逃逸分析举例

我们可以使用go build -gcflags '-m -l' go文件名,来查看逃逸分析的结果

1、参数为interface类型会逃逸

func main() {
   num := 1
   fmt.Println(num)
}

运行结果

PS J:\go-project\test> go build -gcflags '-m -m -l' .\tianji.go
# command-line-arguments
.\tianji.go:7:13: num escapes to heap:
.\tianji.go:7:13:   flow: {storage for ... argument} = &{storage for num}:
.\tianji.go:7:13:     from num (spill) at .\tianji.go:7:13
.\tianji.go:7:13:     from ... argument (slice-literal-element) at .\tianji.go:7:13
.\tianji.go:7:13:   flow: {heap} = {storage for ... argument}:
.\tianji.go:7:13:     from ... argument (spill) at .\tianji.go:7:13
.\tianji.go:7:13:     from fmt.Println(... argument...) (call parameter) at .\tianji.go:7:13
.\tianji.go:7:13: ... argument does not escape
.\tianji.go:7:13: num escapes to heap

原因:func Println(a ...interface{}) (n int, err error),这个函数的入参是interface类型,编译阶段无法确定其具体的参数类型,所以内存分配到堆上

2、变量在函数外部有引用会逃逸

func test() *int {
   num := 1
   return &num
}

func main() {
   _ = test()
}
PS J:\go-project\test> go build -gcflags '-m -m -l' .\tianji.go
# command-line-arguments
.\tianji.go:4:2: num escapes to heap:
.\tianji.go:4:2:   flow: ~r0 = &num:
.\tianji.go:4:2:     from &num (address-of) at .\tianji.go:5:9
.\tianji.go:4:2:     from return &num (return) at .\tianji.go:5:2
.\tianji.go:4:2: moved to heap: num

原因:变量num在函数外部存在引用,函数退出时栈中的内存(栈帧)已经释放,但引用已经被返回,如果通过引用地址取值,在栈中是取不到值的,所以Go为了避免这个情况,会将内存分配到堆上

3、变量占用内存较大时会逃逸

func main() {
  //不会逃逸
  s1 := make([]int, 10, 10)
  for i := 0; i < 10; i++ {
    s1[i] = i
  }

  //会逃逸
  s2 := make([]int, 10000, 10000)
  for i := 0; i < 10000; i++ {
    s2[i] = i
  }
}
.\tianji.go:11:12: make([]int, 10000, 10000) escapes to heap:
.\tianji.go:11:12:   flow: {heap} = &{storage for make([]int, 10000, 10000)}:
.\tianji.go:11:12:     from make([]int, 10000, 10000) (too large for stack) at .\tianji.go:11:12
.\tianji.go:5:12: make([]int, 10, 10) does not escape
.\tianji.go:11:12: make([]int, 10000, 10000) escapes to heap

原因:切片容量过大时,会产生逃逸,内存分配到堆上;容量小时,不会逃逸,内存分配依赖在栈上

4、变量大小不确定时会逃逸

func main() {
	num := 10
	s := make([]int, num, num)
	for i := 0; i < num; i++ {
		s[i] = i
	}
}
PS J:\go-project\test> go build -gcflags '-m -m -l' .\tianji.go
# command-line-arguments
.\tianji.go:5:11: make([]int, num, num) escapes to heap:
.\tianji.go:5:11:   flow: {heap} = &{storage for make([]int, num, num)}:
.\tianji.go:5:11:     from make([]int, num, num) (non-constant size) at .\tianji.go:5:11
.\tianji.go:5:11: make([]int, num, num) escapes to heap

原因:切片的长度和容量,虽然通过声明的变量num来指定了,但在编译阶段是未知的,并不确定num的具体值,所以会逃逸,将内存分配到堆上

Go编译链接过程

从源文件到可执行目标文件的转化过程:

compile

可执行目标文件可以直接在机器上执行。一般而言,先执行一些初始化的工作;找到 main 函数的入口,执行用户写的代码;执行完成后,main 函数退出;再执行一些收尾的工作,整个过程完毕。


面试准备-编译
http://example.com/2023/11/05/面试准备-编译/
作者
Angry Potato
发布于
2023年11月5日
许可协议