简要的来说,go对变量进行逃逸分析的目的是决定变量应该放在栈上还是堆上,当然变量尽可能地要放在栈上(因为goroutine的栈是可以动态扩缩容的,而不是仅限于操作系统设定的,这样当函数返回时变量占用的内存空间就自动回收了),而堆上的内存就需要使用GC机制去进行管理。
对变量进行逃逸分析可以使go语言中在语言层面提供一些语法糖,例如如下写法是很常见的:
func NewString() *string {
s := ""
return &s
}
而如果在C++中这样写,则很明显返回了一个栈变量的指针,使用该指针进行访问是未定义行为:
string* NewString() {
string s;
return &s;
}
因为go在编译过程中对变量s进行了逃逸分析,并决定将变量放在堆中,以便于函数返回之后该变量仍然能被合法的使用,若使用go build -gcflags=-m ...
来编译便可以看到如下说明:
./new_string.go:5:5: moved to heap: s
具体哪些情况下会发生逃逸,可以参考 Go Escape Analysis Flaws。
本文主要是看到这篇博客后对noescape
函数的解析,我在go string源码中看到这个函数是这样使用的:
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
// This was copied from the runtime; see issues 23382 and 7921.
//go:nosplit
//go:nocheckptr
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
func (b *Builder) copyCheck() {
if b.addr == nil {
// This hack works around a failing of Go's escape analysis
// that was causing b to escape and be heap allocated.
// See issue 23382.
// TODO: once issue 7921 is fixed, this should be reverted to
// just "b.addr = b".
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
这个场景其实就是将一个指针赋值给了成员变量,通过noescape
函数避免将变量b
逃逸到堆中而节省内存分配的过程。可以通过几个例子加深对逃逸分析的理解。
例1
package main
type Foo struct {
s *string
}
func main() {
var f Foo
var s string
f.bar(&s)
}
//go:noinline
func (f *Foo) bar(s *string) {
f.s = s
}
这个例子其实就是对string代码中的简化版,s被判定为逃逸,因为出现了非输入到输出的引用(即赋值给了一个成员变量),go编译器为了简便起见,不对有类似情况的变量进行更多的逃逸追踪和分析,而是直接判定为分配到堆中。
例2
package main
type Foo struct {
s *string
}
func main() {
var f Foo
var s string
bar(f, &s)
}
//go:noinline
func bar(f Foo, s *string) {
f.s = s
}
这个例子同样可以赋值给一个成员变量,但是s
却没有逃逸,因为f
变量是传值赋值,没有发生逃逸,分配在栈中,因此f.s
的生命周期也存在于这个栈中,所以对s
的引用是确保在这个函数之内发生的,在bar
函数栈存在的时候,s
必然存在于main
函数栈中,因此s
不需要逃逸。加上这个例子,可以归纳为:当发生非输入到输出的栈外引用时,变量逃逸。
例3
package main
type Foo struct {
s *string
}
func main() {
var f Foo
var s string
s2 := bar1(&s)
f.bar2(s2)
}
//go:noinline
func bar1(s *string) *string {
return s
}
//go:noinline
func (f *Foo) bar2(s *string) {
f.s = s
}
这里例子可以验证上述判断,得到输出:
./m3.go:15:11: leaking param: s to result ~r1 level=0
./m3.go:20:7: f does not escape
./m3.go:20:20: leaking param: s
./m3.go:9:9: moved to heap: s
在bar1
中参数s
流至返回值~r1
,返回值又流入bar2
,在bar2
中被判定为逃逸,因此找到引用的来源s
,判定为逃逸。