看到这篇博文: http://www.zenlife.tk/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
<autogenerated>: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后端后,会有更好的优化吧。 所以现在想优化掉这个的话,只能避免使用接口方法了。