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

  • 相关阅读:
    A1023 Have Fun with Numbers (20分)(大整数四则运算)
    A1096 Consecutive Factors (20分)(质数分解)
    A1078 Hashing (25分)(哈希表、平方探测法)
    A1015 Reversible Primes (20分)(素数判断,进制转换)
    A1081 Rational Sum (20分)
    A1088 Rational Arithmetic (20分)
    A1049 Counting Ones (30分)
    A1008 Elevator (20分)
    A1059 Prime Factors (25分)
    A1155 Heap Paths (30分)
  • 原文地址:https://www.cnblogs.com/qianyuming/p/12844326.html
Copyright © 2011-2022 走看看