GO逃逸分析


写在前面

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 引用的指针,一定发生逃逸;

必然不会逃逸的情况:

  • 指针被未发生逃逸的变量引用;
  • 仅仅在函数内对变量做取址操作,而未将指针传出;

有一些情况可能发生逃逸,也可能不会发生逃逸:

  • 将指针作为入参传给别的函数;这里还是要看指针在被传入的函数中的处理过程,如果发生了上边的三种情况,则会逃逸;否则不会逃逸;

参考

[1]Leader 这样说对吗?还是自己动手验证 Go 逃逸分析


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录