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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335