分享

golang变量逃逸分析

 凤凰苑凶真 2016-12-09

看到这篇博文: http://www./go-allocated-on-heap-or-stack.md ,于是想深入探究一下。

既然fmt.Println会使a、b逃逸,println不会,那就先从fmt.Println入手。

把fmt.Println的相关源码复制到同一个文件内,以让编译器给出具体的逃逸分析报告。 就是src/fmt/print.go和src/fmt/format.go两个文件的内容。

用go tool compile -m生成报告,有几千行,分析之。

先找到定义a变量的那一行,然后往上看:

a.go:1272: reflect.t·2 escapes to heap
a.go:1265: leaking param content: a
a.go:1265: leaking param content: p
a.go:1265: leaking param content: p
a.go:1265: leaking param: p
a.go:1265: leaking param content: a
a.go:1268: (*pp).doPrint p.fmt does not escape
a.go:1272: (*pp).doPrint &reflect.i·2 does not escape
a.go:1274: (*pp).doPrint p.buf does not escape
a.go:1280: (*pp).doPrint p.buf does not escape
a.go:150: (*pp).free ignoring self-assignment to p.buf
a.go:153: ppFree escapes to heap
a.go:153: p escapes to heap
a.go:145: leaking param: p
a.go:256: leaking param content: a
a.go:256: leaking param: w
a.go:268: os.Stdout escapes to heap
a.go:267: leaking param content: a
a.go:17: b escapes to heap
a.go:15: moved to heap: a

从最下一句往上分析:

a被移到堆上
因为b逃逸到堆上
Println的a参数(就是传入的b变量组成的[]interface{})的内容泄漏
这个泄漏不是指内存泄漏,而是指该传入参数的内容的生命期,超过函数调用期,也就是函数返回后,该参数的内容仍然存活
os.Stdout逃逸到堆上
Fprintln的w、a参数都泄漏
*pp.free的p参数(就是receiver)泄漏
该receiver逃逸到heap
ppFree逃逸到heap(这是个全局变量)

把ppFree.Put(p))这行注释掉(因为可能是它引用了最初传入的参数),然后重新go tool compile -m。仍然被移动到堆上。

继续往上分析,然后居然发现这条:

a.go:1272: reflect.t·2 escapes to heap

对应的代码是:

isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
reflect.TypeOf(arg).Kind()居然会导致arg逃逸到堆上。可以用下面的程序验证(TypeOf不会,程序略):
package main

import "reflect"

func main() {
    a := &struct{}{}
    _ = reflect.TypeOf(a).Kind()
}

于是再去看reflect.Type.Kind()的代码,是这样的:

func (t *rtype) Kind() Kind { return Kind(t.kind & kindMask) }

于是问题变成,为什么reflect.TypeOf(arg).Kind()会导致arg逃逸。

按照前面的办法,复制reflect包的内容到文件里的话,会比较麻烦,因为有些函数是定义在runtime包里的。 所以只要一些骨架,能重现就行:

package main

import (
    "unsafe"
)

type Kind uint

const kindMask = (1 << 5) - 1

type Type interface {
    Kind() Kind
}

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

type rtype struct {
    kind uint8 // enumeration for C
}

func (t *rtype) Kind() Kind { return Kind(t.kind & kindMask) }

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

func main() {
    a := &struct{}{}
    _ = TypeOf(a).Kind()
}

上面代码的go tool compile -m 结果是:

a.go:20: can inline toType
a.go:15: can inline TypeOf
a.go:17: inlining call to toType
a.go:31: can inline (*rtype).Kind
a.go:40: inlining call to TypeOf
a.go:40: inlining call to toType
a.go:17: t escapes to heap
a.go:15: leaking param: i to result ~r1 level=0
a.go:16: TypeOf &i does not escape
a.go:24: t escapes to heap
a.go:20: leaking param: t to result ~r1 level=0
a.go:31: (*rtype).Kind t does not escape
a.go:40: t escapes to heap
a.go:40: a escapes to heap
a.go:39: &struct {} literal escapes to heap
a.go:40: main &i does not escape
:1: leaking param: .this

a在堆上分配了。

(分析一小时后……)

结论是,调用interface的方法会导致变量被移到堆上。将上面main里的改成 _ = TypeOf(a).(*rytpe).Kind(),a就不会逃逸了。

同理,下面的程序:

package main

type T interface {
    Foo()
}

type S struct{}

func (s *S) Foo() {}

func main() {
    s := new(S)
    T(s).Foo()
}

s会移到堆上。 所以问题变成,为什么调用接口方法会使引用的变量被放到堆上。

在repo搜索了下,发现是个known issue: https://github.com/golang/go/issues/7213 ,而且缺少关爱。 可能转到SSA后端后,会有更好的优化吧。 所以现在想优化掉这个的话,只能避免使用接口方法了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多