这里填写标题
1. golang 逃逸分析
1.1. 什么是堆和栈?
1.1.1. 栈
在程序中, 每个函数块都会有自己的内存区域用来存自己的局部变量 (内存占用少)、返回地址、返回值之类的数据, 这一块内存区域有特定的结构和寻址方式, 大小在编译时已经确定, 寻址起来也十分迅速, 开销很少。这一块内存地址称为栈。栈是线程级别的, 大小在创建的时候已经确定, 所以当数据太大的时候, 就会发生 “stack overflow”。
1.1.2. 堆
在程序中, 全局变量、内存占用大的局部变量、发生了逃逸的局部变量存在的地方就是堆, 这一块内存没有特定的结构, 也没有固定的大小, 可以根据需要进行调整。简单来说, 有大量数据要存的时候, 就存在堆里面。堆是进程级别的。当一个变量需要分配在堆上的时候, 开销会比较大, 对于 go 这种带 GC 的语言来说, 也会增加 gc 压力, 同时也容易造成内存碎片。
1.1.3. go
go 在一定程度消除了堆和栈的区别, 因为 go 在编译的时候进行逃逸分析, 来决定一个对象放栈上还是放堆上, 不逃逸的对象放栈上, 可能逃逸的放堆上。
1.2. 两种方法判断是否逃逸
1.2.1. 使用编译参数
golang 提供了编译的参数让我们可以直观地看到变量是否发生了逃逸, 只需要在 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)
可以看到, 这里的 00040 有调用 runtime.newobject(SB)
这个方法, 看到这个方法大家就应该懂了!
1.3. 逃逸分析的用处 (为了性能)
最大的好处应该是减少 gc 的压力, 不逃逸的对象分配在栈上, 当函数返回时就回收了资源, 不需要 gc 标记清除。
因为逃逸分析完后可以确定哪些变量可以分配在栈上, 栈的分配比堆快, 性能好。
同步消除, 如果你定义的对象的方法上有同步锁, 但在运行时, 却只有一个线程在访问, 此时逃逸分析后的机器码, 会去掉同步锁运行。
1.4. 扩展
1.4.1. go tool link --help
go build 用 -ldflags
给 go 链接器传入参数, 实际是给 go tool link
的参数, 可以用 go tool link --help
查看可用的参数。
1.4.2. go build -gcflags -gcflags='log=-N -l' main.go
只在编译特定包时需要传递参数, 格式应遵守 “包名 = 参数列表”。
1.4.3. 内联场景
此时, 爱思考的读者可能就会产生疑问: 既然内联优化效果这么显著, 是不是所有的函数调用都可以内联呢? 答案是不可以。因为内联, 其实就是将一个函数调用原地展开, 替换成这个函数的实现。当该函数被多次调用, 就会被多次展开, 这会增加编译后二进制文件的大小。而非内联函数, 只需要保存一份函数体的代码, 然后进行调用。所以, 在空间上, 一般来说使用内联函数会导致生成的可执行文件变大 (但需要考虑内联的代码量、调用次数、维护内联关系的开销)。
1.5. 总结
以上提供了两种方法可以用来判断某个变量是否发生了逃逸, 其中使用编译参数比较简单, 使用汇编比较硬核。通过这两种方法分析完逃逸, 就能进一步优化堆上内存数量, 减轻 GC 压力了。
尽量写出分配在栈上的代码, 堆上的变量变少了, 可以减轻内存分配的开销, 减小 gc 的压力, 提高程序的运行速度。