写在前面
GO逃逸分析是个老生常谈的话题,今天就来好好谈一下
需要了解的
- 不同于 jvm 的运行时逃逸分析,golang 的逃逸分析是在编译期完成的
- Golang 的逃逸分析只针对指针。一个值引用变量如果没有被取址,那么它永远不可能逃逸
- 分析go 代码是否发生逃逸:
go run -gcflags "-m -l" (-m 打印逃逸分析信息,-l 禁止内联编译)
逃逸有两种情况:
- moved to heap: xxx。表示变量xxx发生了逃逸。此时xxx为值类型
- yyy escapes to heap。表示yyy为指针类型。
情况1:在某个函数中 new 或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸
func test() *User{
a := User{}
return &a
}
这种情况最常见、最简单。
情况2:被已经逃逸的变量引用的指针,一定发生逃逸
package main
import (
"fmt"
)
type User struct {
Username *string
Password string
Age int
}
func main() {
a := "aaa"
u := &User{&a, "123", 12}
Call1(u)
}
func Call1(u *User) {
fmt.Printf("%v", u)
}
结果
go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:19:12: leaking param: u
./main.go:20:12: ... argument does not escape
./main.go:14:2: moved to heap: a
./main.go:15:7: &User{...} escapes to heap
&{0xc000014250 123 12}%
看到这里,有的人会有多个疑问:
- 14行变量a为什么会发生逃逸?
- 15行指针类型u为什么会发生逃逸?
有的人会说是因为有函数Call1引用了u,那如果对Call1做一些修改呢
package main
type User struct {
Username *string
Password string
Age int
}
func main() {
a := "aaa"
u := &User{&a, "123", 12}
Call1(u)
}
func Call1(u *User) int {
// fmt.Printf("%v", u)
u.Age = 10
return u.Age * 2
}
结果
go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:15:12: u does not escape
./main.go:11:7: &User{...} does not escape
Call1函数对u进行了读写,但是并没有导致u逃逸。说明将指针类型的变量传到函数中,并在函数中对变量进行读写,并不会导致变量逃逸。
最初的代码在Call1中调用了fmt.Printf函数,我们看看fmt.Printf函数对u做了什么操作。
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
我们发现,Fprintf函数中将参数传递给了p,而newPrinter函数根据情况1分析,p一定发生了逃逸。所以导致入参也发生了逃逸,即最初的代码中指针变量u发生了逃逸。这就是情况2
情况3:被指针类型的 slice、map 和 chan 引用的指针一定发生逃逸
package main
func main() {
a := make([]*int, 1)
b := 12
a[0] = &b
c := make(map[string]*int)
d := 14
c["aaa"] = &d
e := make(chan *int, 1)
f := 15
e <- &f
}
结果
go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:5:2: moved to heap: b
./main.go:9:2: moved to heap: d
./main.go:13:2: moved to heap: f
./main.go:4:11: make([]*int, 1) does not escape
./main.go:8:11: make(map[string]*int) does not escape
注意:
- slice,map和chan本身不会发生逃逸
- 他们的元素类型必须是指针。如果不是指针则不存在逃逸的说法,那就是值拷贝了。
总结
必然发生逃逸:
- 在某个函数中 new 或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸(构造函数返回的指针变量一定逃逸);
- 被已经逃逸的变量引用的指针,一定发生逃逸;
- 被指针类型的 slice、map 和 chan 引用的指针,一定发生逃逸;
必然不会逃逸的情况:
- 指针被未发生逃逸的变量引用;
- 仅仅在函数内对变量做取址操作,而未将指针传出;
有一些情况可能发生逃逸,也可能不会发生逃逸:
- 将指针作为入参传给别的函数;这里还是要看指针在被传入的函数中的处理过程,如果发生了上边的三种情况,则会逃逸;否则不会逃逸;