逃逸分析
在开始这个话题前,你可能要先问为什么要逃逸分析,为什么呢?说到底还是为了性能,那么它是如何影响性能的呢?让我们一探究竟。
1. 堆(heap)内存和栈(stack)内存
在开始前,我们需要弄清楚什么是堆内存,什么又是栈内存?
程序在进行内存分配的时候,有两个去处: 一个是堆、另外一个就是栈,下面我们简单比较下:
-
栈(heap)内存
- 我们常说的函数调用栈也就是这个栈,由于栈结构非常简单,只有两个操作一个push、一个pop,它的性能非常好,速度快;
- 但是栈空间相比于堆非常小,各个操作系统都有自己的最大栈空间大小,这也是为什么我们在写递归的时候为什么会出现堆栈太深错误的原因,我们可以通过
ulimit -a
查看操作系统栈空间大小。 - 如果函数中的局部变量分配在栈上,函数执行结束后,局部变量占用的内存会自动被回收;不需要额外的垃圾回收器
-
堆(stack)内存
- 在堆上主要分配一些占用比较大(超过编译器阀值)、生命周期长(全局变量、指针引用)、占用大小不确定的一些对象。
- 堆上的性能比不上栈
在go中一个变量是分配在栈上还是堆上,是由编译器动态分析决定的。
2. 内存逃逸为什么会影响性能?
好啦,通过上面的对比,我们可以回答本文一开始的问题 —— 逃逸为什么会影响性能了。
我们已经知道内存分配在栈上性能好,而且不需要经过垃圾回收器处理;因此我们希望内存分配尽量在栈上,而尽量少的在堆上;
如果我们的代码中内存分配全部都在分堆上,那可能不太好;这也就是我们为什么要知道逃逸分析的根本原因,这样在我们写代码的时候,就会注意这个问题,从而做到部分优化。
3. 如何逃逸分析?
好消息是,go为我们提供了逃逸分析的工具,我们只需要使用go build -gcflags=-m
就可以进行逃逸分析,我们来看下:
// main.go
package main
import (
"fmt"
)
func main() {
fmt.Println("abc")
}
dongmingyan@pro ⮀ ~/go_playground/play ⮀ go build -gcflags=-m main.go
# command-line-arguments
./main.go:8:6: can inline main
./main.go:9:13: inlining call to fmt.Println
./main.go:9:13: ... argument does not escape
./main.go:9:14: "abc" escapes to heap
看到了吧,escapes to heap
这就表明它逃逸到了堆上哦!
4. 什么场景下会发送逃逸?
4.1 函数返回指针
这个非常好理解,如果返回指针说明,函数调用完后这个变量还不能被销毁,还有其它地方会用到,因此分配到堆上。 代码如下:
package main
import (
"fmt"
)
func main() {
a := hu()
fmt.Println(*a)
}
func hu() *int {
a := 3 // a会逃逸到堆上
return &a
}
4.2 使用interface类型
// main.go
package main
import (
"fmt"
)
func main() {
// abc 发生逃逸 因为Println它接受的是一个interface
fmt.Println("abc")
}
4.3 超过一定大小或者未知大小
package main
func main() {
size8192()
size8193()
anySize(10)
}
func size8192() {
s := make([]int, 8192)
for i := 0; i < 8192; i++ {
s[i] = i
}
}
func size8193() {
s := make([]int, 8193) // > 64KB 超过编译器阀值 发生逃逸
for i := 0; i < 8193; i++ {
s[i] = i
}
}
func anySize(n int) {
// 由于n值未知发生逃逸
s := make([]int, n)
for i := 0; i < n; i++ {
s[i] = i
}
}
4.4 闭包使用外部局部变量
package main
func main() {
h := hu()
h()
}
func hu() func() {
// i被闭包函数使用移到堆上
i := 0
return func() {
i++
println(i)
}
}