0%

《Golang》逃逸分析

首先要明确两个概念:

  • 堆:是进程中一段映射到物理内存页的虚拟地址空间,是由运行时中的堆管理器向操作系统批发的,进程中申请内存其实是向堆管理器申请。
  • 栈:同样也是进程中一段映射到物理内存页的虚拟地址空间,但用途不一样。
    • 进程的栈是程序编译后就确定了的,就是栈段所处的虚拟空间,进程的堆则是由运行时的堆管理器从操作系统中即时申请的
    • 线程的栈是从运行时的堆管理器中申请的
    • 进程的内核栈则是操作系统为每个进程分配的一小段空间,为了支持进程进入系统调用后的代码的执行。进程的内核栈处于内核虚拟空间中

Golang中栈一般是指g的栈,每个g拥有自己的栈空间,g中的多数函数拥有自己的栈帧,栈帧在函数开始时分配,函数结束时自动清理。栈中内存的分配与清理非常高效且处理干净,不留任何垃圾碎片,也无需gc的干预,所以能在栈上分配的内存应当尽量栈上分配。

Golang中的栈与堆是Go运行时自己维护的两种结构,Golang向操作系统申请一些内存然后自己管理分发,g申请的栈空间就是由运行时分发的

因为栈与堆的性能差异,内存的分配应当合理的分配在堆或者栈上,不应该分配到堆上的就不要分配到堆上。

那么怎么合理分配栈与堆呢,逃逸分析出现了

简介

在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。

发生逃逸的情况

Golang的逃逸分析是在编译期完成的,在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上(放堆上的变量会被编译成调用runtime.newobject函数的汇编代码)。以下两种方式可以知道是否发生了逃逸

Golang编译器只会对指针类型的变量进行逃逸分析,因为值类型变量永远不可能发生逃逸,只会分配在栈上,值类型变量传递时都会进行值拷贝

1
go build -gcflags "-m -l" ./bin/main/   // -m可以打印逃逸日志,多个-m可以显示更详细的日志。-l禁用内联优化
1
go tool compile -S ./bin/main/main.go | grep newobject

函数内指针类型变量太大

函数内如果一个变量非常大,那么它应该被分配到堆上而不是栈上

1
2
3
4
5
6
7
8
9
10
11
package main


func main() {
test()
}

func test() {
_ = make([]string, 1000) // 不逃逸
_ = make([]string, 10000) // 发生逃逸
}
1
2
3
bash-3.2$ go build -gcflags "-m -l" ./bin/main/
bin/main/main.go:9:10: make([]string, 1000) does not escape
bin/main/main.go:10:10: make([]string, 10000) escapes to heap

函数内变量可能被函数外部使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main


func main() {
_ = test()
}

type Test struct {

}

func test() *Test {
return &Test{} // 发生逃逸
}
1
2
bash-3.2$ go build -gcflags "-m -l" ./bin/main/
bin/main/main.go:13:9: &Test literal escapes to heap

test函数中的返回值是一个指针,不存在返回值的值拷贝,因为返回值可能被函数外部引用(虽然本例中没有),所以此处会发生逃逸,Test{}对象会被分配到堆上,然后返回指向它的指针。

假如Test{}对象分配在栈上,test函数结束后,整个栈帧会被释放,那么Test{}对象自然会被清理,如果test函数的返回值被使用,这时就会发生空指针异常,这很明显是不允许的。

下篇预告

栈检查与栈扩容




微信关注我,及时接收最新技术文章