1.1. 什么是堆和栈?
1.1.1. 栈
在程序中, 每个函数块都会有自己的内存区域用来存自己的局部变量 (内存占用少)、返回地址、返回值之类的数据, 这一块内存区域有特定的结构和寻址方式, 大小在编译时已经确定, 寻址起来也十分迅速, 开销很少。这一块内存地址称为栈。栈是线程级别的, 大小在创建的时候已经确定, 所以当数据太大的时候, 就会发生 “stack overflow”。
1.1.2. 堆
在程序中, 全局变量、内存占用大的局部变量、发生了逃逸的局部变量存在的地方就是堆, 这一块内存没有特定的结构, 也没有固定的大小, 可以根据需要进行调整。简单来说, 有大量数据要存的时候, 就存在堆里面。堆是进程级别的。当一个变量需要分配在堆上的时候, 开销会比较大, 对于 go 这种带 GC 的语言来说, 也会增加 gc 压力, 同时也容易造成内存碎片。
1.1.3. go
go 在一定程度消除了堆和栈的区别, 因为 go 在编译的时候进行逃逸分析, 来决定一个对象放栈上还是放堆上, 不逃逸的对象放栈上, 可能逃逸的放堆上。
1.2. 两种方法判断是否逃逸
1.2.1. 使用编译参数
go build-gcflags '-m -l'
# go tool compile --help
-l disable inlining 禁止内联优化
-m print optimization decisions 可以检查代码的编译优化情况, 包括逃逸情况和函数是否内联。
package main
func f1() *int {
i := 1
return &i
}
func main() {
a := f1()
*a++
}
# command-line-arguments
.\main.go:4:2: moved to heap: i
这样可以很直观地看到在第 4 行, i 发生了逃逸, 内存会分配在堆上。
除了使用编译参数之外, 我们还可以使用一种更底层的, 更硬核, 也更准确的方式来判断一个对象是否逃逸, 那就是: 直接看汇编!
1.2.2. 使用汇编
go tool compile -S
$ go tool compile -S escape.go | grep escape.go:10
0x001d 00029 (escape.go:10) PCDATA $2, $1
0x001d 00029 (escape.go:10) PCDATA $0, $0
0x001d 00029 (escape.go:10) LEAQ type.int(SB), AX
0x0024 00036 (escape.go:10) PCDATA $2, $0
0x0024 00036 (escape.go:10) MOVQ AX, (SP)
0x0028 00040 (escape.go:10) CALL runtime.newobject(SB)
0x002d 00045 (escape.go:10) PCDATA $2, $1
0x002d 00045 (escape.go:10) MOVQ 8(SP), AX
0x0032 00050 (escape.go:10) MOVQ $1, (AX)
runtime.newobject(SB)
1.3. 逃逸分析的用处 (为了性能)
最大的好处应该是减少 gc 的压力, 不逃逸的对象分配在栈上, 当函数返回时就回收了资源, 不需要 gc 标记清除。
因为逃逸分析完后可以确定哪些变量可以分配在栈上, 栈的分配比堆快, 性能好。
同步消除, 如果你定义的对象的方法上有同步锁, 但在运行时, 却只有一个线程在访问, 此时逃逸分析后的机器码, 会去掉同步锁运行。
1.4. 扩展
go tool link --help
-ldflagsgo tool linkgo tool link --help
go build -gcflags -gcflags='log=-N -l' main.go
只在编译特定包时需要传递参数, 格式应遵守 “包名 = 参数列表”。
1.4.3. 内联场景
此时, 爱思考的读者可能就会产生疑问: 既然内联优化效果这么显著, 是不是所有的函数调用都可以内联呢? 答案是不可以。因为内联, 其实就是将一个函数调用原地展开, 替换成这个函数的实现。当该函数被多次调用, 就会被多次展开, 这会增加编译后二进制文件的大小。而非内联函数, 只需要保存一份函数体的代码, 然后进行调用。所以, 在空间上, 一般来说使用内联函数会导致生成的可执行文件变大 (但需要考虑内联的代码量、调用次数、维护内联关系的开销)。
1.5. 总结
以上提供了两种方法可以用来判断某个变量是否发生了逃逸, 其中使用编译参数比较简单, 使用汇编比较硬核。通过这两种方法分析完逃逸, 就能进一步优化堆上内存数量, 减轻 GC 压力了。
尽量写出分配在栈上的代码, 堆上的变量变少了, 可以减轻内存分配的开销, 减小 gc 的压力, 提高程序的运行速度。