Golang 逃逸分析

避免逃逸的好处

  1. 最大的好处应该是减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
  2. 因为逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好
  3. 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

分析工具

压测命令:go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
编译命令:go build -gcflags "-N -m -m -l"
解释:

  • -gcflags
    -N 禁止编译优化
    -l 禁止内联,禁止内联也可以一定程度上减小可执行程序大小
    -m 逃逸分析 , 可以重复最多四次
    -benchmem 压测时打印内存分配统计

go版本:1.12.1

不同版本的gc处理机制可能会不同

接下来根据逃逸类型来逐个进行演示

案例分析

  1. 取址符号&并return,会产生逃逸
package main
type UserData struct {
   Name  string
}
func main() {
   var info UserData
   info.Name = "WilburXu"
   _ = GetUserInfo(info)
}
func GetUserInfo(userInfo UserData) *UserData {
   return &userInfo  //发生引用,产生逃逸逃逸
}

修正:

func main() {
   var info UserData
   info.Name = "WilburXu"
   _ = GetUserInfo(&info)
}
func GetUserInfo(userInfo *UserData) *UserData {
   return userInfo //相当于值传递,只不过是传递的地址的拷贝,指向了同一个目标
}

原因:GetUserInfo里面发生了引用, 去掉引用后变量 userInfo 变成了 leaking param,即流式变量,由于该变量仅在子函数中使用,随着函数退出自动销毁

  1. 指针间接赋值 (star-dot-equals)
package main

import (
    "testing"
)

func BenchmarkAssignmentIndirect(b *testing.B) {
    type X struct {
        p *int
    }
    for i := 0; i < b.N; i++ {
        var i1 int
        x1 := &X{
            p: &i1, //结构体内部赋值,不逃逸
        }
        _ = x1

        var i2 int
        x2 := &X{}
        x2.p = &i2 // 结构体外部.操作赋值,属间接调用,逃逸
    }
}
[developer@localhost test]$ go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
# test [test.test]
./main_test.go:20:10: &i2 escapes to heap
./main_test.go:20:10:   from x2.p (star-dot-equals) at ./main_test.go:20:8
./main_test.go:18:7: moved to heap: i2
./main_test.go:7:34: BenchmarkAssignmentIndirect b does not escape
./main_test.go:14:7: BenchmarkAssignmentIndirect &i1 does not escape
./main_test.go:14:4: BenchmarkAssignmentIndirect &X literal does not escape
./main_test.go:19:9: BenchmarkAssignmentIndirect &X literal does not escape

注:如果 “.操作的变量”不是指针,则不会发生逃逸

  1. interface{}类型赋值:( passed to call[argument escapes])
package main
type User struct {
   name string
}
func main() {
   var a int
   interfaceA(&a)

   name := "hello world"  
   MyPrintln(name) //name 逃逸, 通过interface传递调用赋值
}
func MyPrintln(one interface{}) (n int, err error) {
   userInfo := new(User) 
   userInfo.name = one.(string) //结构体(复杂类型),间接赋值发生逃逸
   _ = userInfo
   return
}
func interfaceA(a interface{}){
   var c interface{}
   c = a.(int) //简单变量不逃逸
   _ = c  //防止编译错误
}
[developer@localhost test]$ go build -gcflags "-N -m -m -l"
# test
./main.go:18:17: interfaceA a does not escape
./main.go:20:4: interfaceA a.(int) does not escape
./main.go:12:16: leaking param: one
./main.go:12:16:    from userInfo.name (star-dot-equals) at ./main.go:14:16
./main.go:13:17: MyPrintln new(User) does not escape
./main.go:10:11: name escapes to heap
./main.go:10:11:    from name (passed to call[argument escapes]) at ./main.go:10:11
./main.go:7:13: main &a does not escape
./main.go:7:13: main &a does not escape

修正:userInfo 直接赋值

package main
type User struct {
   name string
}
func main() {
   name := "hello world"
   MyPrintln(name)
}
func MyPrintln(one interface{}) (n int, err error) {
   //直接赋值   
   userInfo := &User{
      name : one.(string),
   }
   _ = userInfo
   return
}
[developer@localhost test]$ go build -gcflags "-N -m -m -l"
# test
./main.go:9:16: MyPrintln one does not escape
./main.go:12:3: MyPrintln &User literal does not escape
./main.go:7:11: main name does not escape
  1. 间接调用 (parameter to indirect call)
eg:1
func BenchmarkLiteralFunctions(b *testing.B) {
   for i := 0; i < b.N; i++ {
      var y1 int
      foo(&y1, 42)

      var y2 int
      func(p *int, x int) {
         *p = x
      }(&y2, 42)

      var y3 int
      p := foo
      p(&y3, 42) // 变量y3逃逸
   }
}
func foo(p *int, x int) {
   *p = x
}


func BenchmarkHandler(b *testing.B) {
   // Setup route with specific handler.
   h := func(w http.ResponseWriter, r *http.Request) error {
      // fmt.Println("Specific Request Handler")
      return nil
   }
   route := wrapHandler(h)
   // Execute route.
   for i := 0; i < b.N; i++ {
      var r http.Request
      route(nil, &r) //r发生逃逸,间接调用h函数, 
      //修正方法:用h 直接替换route函数
   }
}
type Handler func(w http.ResponseWriter, r *http.Request) error
func wrapHandler(h Handler) Handler {
   f := func(w http.ResponseWriter, r *http.Request) error {
      return h(w, r)
   }
   return f
}
  1. map & slice 指针赋值 (value of map put , slice-element-equals)
package main

import (
    "testing"
)
func BenchmarkSliceMapAssignment(b *testing.B) {
    for i := 0; i < b.N; i++ {
        m := make(map[int]*int)
        var x1 int
        m[0] = &x1 //x1逃逸

        s := make([]*int, 1)
        var x2 int
        s[0] = &x2 //x2逃逸
    }
}
[developer@localhost test]$ go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
# test [test.test]
./main_test.go:10:10: &x1 escapes to heap
./main_test.go:10:10:   from m[0] (value of map put) at ./main_test.go:10:8
./main_test.go:9:7: moved to heap: x1
./main_test.go:14:10: &x2 escapes to heap
./main_test.go:14:10:   from s[0] (slice-element-equals) at ./main_test.go:14:8
./main_test.go:13:7: moved to heap: x2
./main_test.go:6:34: BenchmarkSliceMapAssignment b does not escape
./main_test.go:8:12: BenchmarkSliceMapAssignment make(map[int]*int) does not escape
./main_test.go:12:12: BenchmarkSliceMapAssignment make([]*int, 1) does not escape

修正:map[int]int =》 map[int]int; make([]int, 1) =》 make([]int, 1)**

package main

import (
    "testing"
)

type S struct {
    I int
}
type myMap map[string]S
type myMapPtr map[string]*S
func BenchmarkValueType(b *testing.B) {
    for i := 0; i < b.N; i++ {
        mm := make(myMap)
        mm["sam"] = S{I:10}
        mm.update("sam")
    }
}
func BenchmarkPtrType(b *testing.B) {
    for i := 0; i < b.N; i++ {
        mm := make(myMapPtr)
        mm["sam"] = &S{I:10}  //逃逸
        mm.update("sam")
    }
}
func (s myMap) update(key string){
    sss := s[key]
    sss.I = 100
    s[key] = sss
}
func (s myMapPtr) update(key string){
    s[key].I = 100
}
[developer@localhost test]$ go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
# test [test.test]
./main_test.go:26:23: leaking param: key
./main_test.go:26:23:   from s[key] (key of map put) at ./main_test.go:29:3
./main_test.go:26:7: myMap.update s does not escape
./main_test.go:12:25: BenchmarkValueType b does not escape
./main_test.go:14:13: BenchmarkValueType make(myMap) does not escape
./main_test.go:31:7: myMapPtr.update s does not escape
./main_test.go:31:26: myMapPtr.update key does not escape
./main_test.go:22:18: &S literal escapes to heap
./main_test.go:22:18:   from mm["sam"] (value of map put) at ./main_test.go:22:13
./main_test.go:19:23: BenchmarkPtrType b does not escape
./main_test.go:21:13: BenchmarkPtrType make(myMapPtr) does not escape
./main_test.go:26:23: leaking param: key
./main_test.go:26:23:   from key (passed to call[argument escapes]) at <autogenerated>:1
<autogenerated>:1: (*myMap).update .this does not escape
<autogenerated>:1: (*myMapPtr).update .this does not escape
./main_test.go:31:26: (*myMapPtr).update key does not escape
# test.test
/tmp/go-build766679195/b001/_testmain.go:42:42: testdeps.TestDeps literal escapes to heap
/tmp/go-build766679195/b001/_testmain.go:42:42:     from testdeps.TestDeps literal (passed to call[argument escapes]) at $WORK/b001/_testmain.go:42:24
goos: linux
goarch: amd64
pkg: test
BenchmarkValueType  20000000           100 ns/op           0 B/op          0 allocs/op
BenchmarkPtrType    20000000            89.6 ns/op         8 B/op          1 allocs/op
PASS
ok      test    4.003s

数逃逸分析报告可以直观看出,指针类型的效率略高(我用切片测试反而值类型效率更高,有兴趣的可以测试下),但由于逃逸到堆上分配,废弃时产生垃圾,所以在使用map 和切片时,如果变量只是局部的变量,则尽量用值类型存储,否则频繁创建则会给gc造成压力。

  1. 接收者间接调用 (receiver in indirect call)
type Iface interface {
   Method()
}
type X struct {
   name string
}
func (x X) Method() {}
func BenchmarkInterfaces(b *testing.B) {
   for i := 0; i < b.N; i++ {
      x1 := X{"bill"}
      var i1 Iface = x1
      var i2 Iface = &x1
      i1.Method()  //间接调用,因为中间设计到一次接口转换 receiver in indirect call
      i2.Method()   //...
      x2 := X{"bill"}
      foo(x2)  //间接传递变量 passed to call[argument escapes] 
      foo(&x2) //间接传递变量 passed to call[argument escapes] 
   }
}
func foo(i Iface) {
   i.Method()
}

即通过接口调用会产生逃逸

总结:

  • 局部函数需要返回参数的,如果确定返回后也是局部变量,则避免逃逸的方法是通过入参的形式传递
  • 慎用指针
  • 空接口interface{} 虽然提高了灵活性,但是变量传递会发生逃逸
  • map & slice 初始化时,预估容量,避免由扩展导致的内存分配。但是如果太大(10000)也会逃逸,因为栈的空间是有限的

最后,在写业务代码时,对有性能要求的功能一定要写单元测试,这样可以直接限定我们逃逸分析的区域,避免产生大量不必要的log日志影响查看。
参考:https://studygolang.com/articles/12396?fr=sidebar