zoukankan      html  css  js  c++  java
  • Go源码分析: 逃逸分析

    原文链接

    什么是逃逸分析

    逃逸分析(Escape Analysis)是Go在编译程序时执行的过程, 由编译器通过分析, 决定变量应当分配在栈上还是堆上.

    在编译中进行逃逸分析

    目前有代码如下:

    package main
    
    import (
    	"fmt"
    )
    
    type User struct {
    	name string
    }
    
    func GetUsername(u *User) string {
    	return u.name
    }
    
    func escapeSimple() int {
    	i := 1
    	j := i + 1
    	return j
    }
    
    func main() {
    	fmt.Println(escapeSimple())
    }
    

    通过在编译时增加gcflags参数, 使用类似如下命令编译:

    go build -gcflags '-m -N -l' ./advanced/cmd/escape-analysis
    

    然后获得输出如下:

    # github.com/tangyanhan/u235/advanced/cmd/escape-analysis
    advanced/cmd/escape-analysis/main.go:11:18: leaking param: u to result ~r1 level=1
    advanced/cmd/escape-analysis/main.go:22:13: main ... argument does not escape
    advanced/cmd/escape-analysis/main.go:22:26: escapeSimple() escapes to heap
    

    这些信息,表明 GetUsername 将参数"泄露"到了返回值中, 而 escapeSimple 则逃逸到了堆中.

    编译时参数是怎么来的?

    在 go build 命令执行时, 其实包含了编译(compile), 连接(link)等多个步骤, 这里 -gcflags 实际上是传递给 go tool compile的参数, 相关列表可以通过以下命令获得:

    go tool compile --help
    

    类似的, 在连接时, 通过-ldflags 传递给go tool link, 对应参数列表, 可以通过以下命令获得:

    go tool link --help
    

    源码中的逃逸分析

    在Go源码中, 通过注释解释了逃逸分析的运行机制. 1.14源码中, 这段注释出现在 src/cmd/compile/internal/gc/escape.go 开头.

    第一段如下:

    这里我们通过分析函数来决定Go变量应当分配到栈上, 包括那些明确调用了 new 和 make 的语句. 我们必须要保证的两点不变条件是:
    1. 指向栈对象的指针不能被存在堆里
    2. 指向栈对象的指针,生命周期不能超出栈对象本身(因为声明栈对象的函数在返回时已经摧毁栈帧,或者它的空间被复用于循环中的局部变量)
    

    这里揭示了几点:

    • 即使是明确调用 new/make 创建出来的变量, 也可能被分配到栈上
    • 当函数中的变量被返回时, 它将不可能被分配到栈上. 循环中的局部变量不会被分配到堆上(一般情况)

    new/make 不一定逃逸

    对于第一点, 以下面的代码为例, 就会发现 GetUsername 中通过 new 创建出的 p, 实际生命周期并没有超出函数范围. 而 return u.name, 导致参数 u 被抛出了范围.

    func GetUsername(u *User) string {
    	p := new(User)
    	p.name = "John"
    	fmt.Println(p.name)
    	return u.name
    }
    

    而分析结果也如我们所料:

    advanced/cmd/escape-analysis/main.go:11:18: leaking param: u to result ~r1 level=1
    advanced/cmd/escape-analysis/main.go:12:10: GetUsername new(User) does not escape
    advanced/cmd/escape-analysis/main.go:14:13: GetUsername ... argument does not escape
    

    循环逃逸

    func loop() {
    	m := map[string]string{
    		"a": "A",
    		"b": "B",
    		"c": "C",
    	}
    	for k, v := range m {
    		fmt.Println(k, v)
    	}
    }
    

    分析结果如下:

    advanced/cmd/escape-analysis/main.go:27:24: loop map[string]string literal does not escape
    advanced/cmd/escape-analysis/main.go:33:14: loop ... argument does not escape
    advanced/cmd/escape-analysis/main.go:33:14: k escapes to heap
    advanced/cmd/escape-analysis/main.go:33:14: v escapes to heap
    

    首先, m 虽然是个map, 但它很小, 而且 loop 自产自销, 在栈空间足够的情况下, 是可以使用的.
    其次, k, v 在循环中被复用, 因此也被分配到了堆上.

    循环逃逸带来的一个小问题

    假设现有 Obj Slice, 其内部的Val如下: 1, 2, 3, 4, 5. 通过下面代码, 调用 print 后打印结果是什么?

    type Obj struct {
        Val int
    }
    
    func print(objs []*Obj) {
        for _, v := range objs {
            defer fmt.Println(v.Val)
            defer func() {
                fmt.Println(v.Val)
            }()
        }
    }
    

    闭包引用变量逃逸

    对于生命周期, 主要就是围绕着返回值, 那么如果是闭包呢?

    func closure() {
    	x := "hello"
    	fn := func() {
    		fmt.Println(x)
    	}
    	fn()
    }
    

    分析发现, x 被分配到了堆上, 闭包中引用的变量会被分配到堆上.

    advanced/cmd/escape-analysis/main.go:20:8: closure func literal does not escape
    advanced/cmd/escape-analysis/main.go:21:14: closure.func1 ... argument does not escape
    advanced/cmd/escape-analysis/main.go:21:14: x escapes to heap
    

    这里又引出了另一个问题, 关于闭包的实现问题... 以后再说.

    new/make 等被判定分配到栈上的阈值是多少?

    我们知道, 栈的大小是有限的, 如果系统限制栈长度为8mb, 那么我们就不可能分配一个10mb的slice到栈上. 之前我们提到过有些语句, 即使我们明确使用了new/make, 创建出的对象还是可能被分配到栈上.

    那么问题来了, Go依据什么决定new/make分配到栈上呢?

    1.14 src/cmd/compile/internal/gc/esc.go:mustHeapALloc 描述了这个逻辑:

    func mustHeapAlloc(n *Node) bool {
    	// TODO(mdempsky): Cleanup this mess.
    	return n.Type != nil &&
    		(n.Type.Width > maxStackVarSize ||
    			(n.Op == ONEW || n.Op == OPTRLIT) && n.Type.Elem().Width >= maxImplicitStackVarSize ||
    			n.Op == OMAKESLICE && !isSmallMakeSlice(n))
    }
    
    // ...
    var (
    	// maximum size variable which we will allocate on the stack.
    	// This limit is for explicit variable declarations like "var x T" or "x := ...".
    	// Note: the flag smallframes can update this value.
    	maxStackVarSize = int64(10 * 1024 * 1024)
    
    	// maximum size of implicit variables that we will allocate on the stack.
    	//   p := new(T)          allocating T on the stack
    	//   p := &T{}            allocating T on the stack
    	//   s := make([]T, n)    allocating [n]T on the stack
    	//   s := []byte("...")   allocating [n]byte on the stack
    	// Note: the flag smallframes can update this value.
    	maxImplicitStackVarSize = int64(64 * 1024)
    )
    
    1. Object 自身长度不能超过栈长度
    2. Object不超过最大栈变量长度(目前64位linux上是 64k)

    事实上, slice/map 只是一个普通的struct, 往往实际分配都在堆上. map 在逃逸分析时只是被作为一个普通的struct, 因为其内元素大小/增长, 并不会影响其struct本身.

    slice略微特殊, slice在分配和增长中有一套自己的逻辑, 如果对很小的slice也统统分配到堆上, 可能会造成大量的内存碎片. slice目前的分配阈值是64k(linux 64, Go1.14, 且未通过smallframes变更 maxImplicitStackVarSize的值). 即不超过64k, 且经过逃逸分析未逃逸的slice, 会被分配到栈上, 而不是堆上.

    相关代码

    相关代码放在我的github仓库中: advanced/cmd/escape-analysis/main.go

  • 相关阅读:
    vue笔记-inheritAttrs及$attr表示含义(一)
    Springboot项目使用junit-test(@Test)报错原因汇总
    @Configuration的使用
    Spring-RabbitMQ实现商品的同步(后台系统)
    RabbitMQ持久化和非持久化
    spring-AMQP-RabbitMQ
    RabbitMQ的5种模式
    RabbitMQ消息队列+安装+工具介绍
    Mina整体体系结构分析
    Mina入门级客户端程序实现telnet程序
  • 原文地址:https://www.cnblogs.com/qianyuming/p/12844326.html
Copyright © 2011-2022 走看看