前言
这是 Go 十大常见谬误系列的第 3 篇:Go 指针的性能问题和内存逃逸。素材来源于 Go 布道者,现 Docker 公司资深工程师 Teiva Harsanyi。
本文波及的源代码全副开源在:Go 十大常见谬误源代码,欢送大家关注公众号,及时获取本系列最新更新。
场景
咱们晓得,函数参数和返回值能够应用变量或者指向变量的指针。
Go 初学者容易有一种误会:
- 认为函数参数和返回值如果应用变量的值会对整个变量做拷贝,速度慢
- 认为函数参数和返回值如果应用指针类型,只须要拷贝内存地址,速度更快
但事实真的是这样么?咱们能够看下这段代码 this example,做性能测试的后果如下:
$ go test -bench .
goos: darwin
goarch: amd64
pkg: pointer
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
BenchmarkByPointer-4 6473781 178.2 ns/op
BenchmarkByValue-4 21760696 47.11 ns/op
PASS
ok pointer 2.894s
能够看出,参数和返回值都用指针的函数比参数和返回值都用变量值的函数慢很多,前者的耗时是后者的 4 倍。
初学者看到这个,可能会感觉有点反直觉,为什么会这样呢?
这和 Go 对 stack(栈)和 heap(堆)的内存治理有关系,变量调配在 stack 上还是 heap 上,对性能是会有影响的。
- stack 上分配内存效率比 heap 更高,而且 stack 上调配的内存不必做 GC,超出了作用域,就主动回收内存。
- 放在 heap 上的内存,须要由 GC 来做内存回收,而且容易产生内存碎片。
- 编译器在编译期决定变量调配在 stack 还是 heap 上,须要做逃逸剖析(escape analysis),逃逸剖析在编译阶段就实现了。
什么是逃逸剖析呢?
Go 编译器解析源代码,决定哪些变量调配在 stack 内存空间,哪些变量调配在 heap 内存空间的过程就叫做逃逸剖析,属于 Go 代码编译的一个分析阶段。
通过逃逸剖析,编译器会尽可能把能调配在栈上的对象调配在栈上,防止堆内存频繁 GC 垃圾回收带来的零碎开销,影响程序性能。
案例 1
咱们看上面的代码:其中构造体 foo
的能够参考 this example。
func getFooValue() foo {
var result foo
// Do something
return result
}
变量 result
定义的时候会在这个 goroutine 的 stack 上调配 result
的内存空间。
当函数返回时,getFooValue
的调用方如果有接管返回值,那 result
的值会被拷贝给对应的接管变量。
stack 上变量 result
的内存空间会被开释(标记为不可用,不能再被拜访,除非这块空间再次被调配给其它变量)。
留神 :本案例的构造体foo
占用的内存空间比拟小,约 0.3KB,goroutine 的 stack 空间足够存储,如果 foo
占用的空间过大,在 stack 里存储不了,就会分配内存到 heap 上。
案例 2
咱们看上面的代码:
func getFooPointer() *foo {
var result foo
// Do something
return &result
}
函数 getFooPointer
因为返回的是一个指针,如果变量 result
调配在 stack 上,那函数返回后,result
的内存空间会被开释,就会导致承受函数返回值的变量无法访问本来 result
的内存空间,成为一个悬浮指针(dangling pointer)。
所以这种状况会产生内存逃逸,result
会调配在 heap 上,而不是 stack 上。
案例 3
咱们看上面的代码:
func main() {p := &foo{}
f(p)
}
指针变量 p
是函数 f
的实参,因为咱们是在 main 所在的 goroutine 里调用函数 f
,并没有跨 goroutine,所以指针变量p
调配在 stack 上就能够,不须要调配在 heap 上。
总结
那咱们怎么晓得到底变量是调配在 stack 上还是 head 上呢?
Go 官网给的说法是:
- 从程序正确性的角度而言,你不须要关怀变量是调配在 stack 上还是 heap 上。变量调配在哪块内存空间不扭转 Go 语言的语义。
- 从程序性能的角度而言,你能够关怀变量到底是调配在 stack 上还是 heap 上,因为正如上文所言,变量存储的地位是对性能有影响的。
How do I know whether a variable is allocated on the heap or the stack?
From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
一般而言,遇到以下状况会产生逃逸行为,Go 编译器会将变量存储在 heap 上
- 函数内局部变量在函数内部被援用
- 接口 (interface) 类型的变量
- size 未知或者动态变化的变量,如 slice,map,channel,[]byte 等
- size 过大的局部变量,因为 stack 内存空间比拟小。
此外,咱们还能够借助内存逃逸剖析工具来帮忙咱们。
因为内存逃逸剖析是编译器在编译期就实现的,能够应用以编译下命令来做内存逃逸剖析:
go build -gcflags="-m"
,能够展现逃逸剖析、内联优化等各种优化后果。go build -gcflags="-m -l"
,-l
会禁用内联优化,这样能够过滤掉内联优化的后果展现,让咱们能够关注逃逸剖析的后果。go build -gcflags="-m -m"
,多一个-m
会展现更具体的剖析后果。
举荐浏览
- Go 十大常见谬误第 1 篇:未知枚举值
- Go 十大常见谬误第 2 篇:benchmark 性能测试的坑
- Go 栈和指针的语法机制
- 逃逸剖析原理 by ArdanLabs
- 逃逸剖析原理 by Gopher Con
开源地址
文章和示例代码开源在 GitHub: Go 语言高级、中级和高级教程。
公众号:coding 进阶。关注公众号能够获取最新 Go 面试题和技术栈。
集体网站:Jincheng’s Blog。
知乎:无忌。
福利
我为大家整顿了一份后端开发学习材料礼包,蕴含编程语言入门到进阶常识(Go、C++、Python)、后端开发技术栈、面试题等。
关注公众号「coding 进阶」,发送音讯 backend 支付材料礼包,这份材料会不定期更新,退出我感觉有价值的材料。还能够发送音讯「进群」,和同行一起交流学习,答疑解惑。
References
- https://itnext.io/the-top-10-…
- https://www.ardanlabs.com/blo…
- https://www.ardanlabs.com/blo…
- https://www.youtube.com/watch…