Dawn's Blogs

分享技术 记录成长

0%

Go语言高性能编程 (6) 编译优化——逃逸分析

栈和堆

Golang 程序会在两个地方分配内存:每一个 goroutine 的栈和全局堆。但是垃圾回收机制在栈和堆上的性能差异非常大:

  • 在栈上分配时,函数执行结束时自动回收。在栈上分配和回收内存的开销很低,只需要 2 个 CPU 指令:PUSH 和 POP。
  • 在堆上分配时,在某个时间点上利用垃圾回收机制进行回收。在堆上分配内存,一个很大的额外开销则是垃圾回收,需要经过垃圾回收算法的计算才能回收堆上的对象

逃逸分析

在函数内部声明变量时,需要知道这个变量分配在栈上还是堆上。

编译器决定内存分配位置的方式,就称之为逃逸分析。逃逸分析由编译器完成,作用于编译阶段。

发生场景

发生逃逸分析的场景:

  • 指针逃逸
  • interface{} 动态类型逃逸
  • 栈空间不足
  • 闭包

指针逃逸

这是最常见,也是最好理解的情况。

当函数返回指针时,这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,这个指针所指向的变量就需要分配在堆空间上

interface{} 逃逸

Golang 中空接口可以表示任意类型,如果函数的参数类型为空接口类型,因为编译期间很难确定其参数的具体类型(是否是指针还未知),也会发生逃逸。

栈空间不足

操作系统对内核线程使用的栈空间是有大小限制的,当 goroutine 被调度时会绑定一个内核级线程去执行,所以 goroutine 的栈空间大小不能超过操作系统的限制

对 Go 编译器而言,超过一定大小(或者无法确定大小,如切片)的局部变量将逃逸到堆上

闭包

因为闭包函数可以引用函数体之外的变量,所以在闭包函数内引用的变量也会逃逸分析到堆上。

如:Increase 函数内部有一个闭包函数引用了外部变量 n,所以 n 不能因为闭包函数的退出而被销毁,所以 n 被分配在了堆上。当 in 对象被销毁时,n 才会被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}

func main() {
in := Increase()
fmt.Println(in()) // 1
fmt.Println(in()) // 2
}

传值 vs 传指针

传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收的负担

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。