看到這篇博文: http://www./go-allocated-on-heap-or-stack.md ,于是想深入探究一下。
既然fmt.Println會使a、b逃逸,println不會,那就先從fmt.Println入手。
把fmt.Println的相關(guān)源碼復(fù)制到同一個文件內(nèi),以讓編譯器給出具體的逃逸分析報告。
就是src/fmt/print.go和src/fmt/format.go兩個文件的內(nèi)容。
用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參數(shù)(就是傳入的b變量組成的[]interface{})的內(nèi)容泄漏
這個泄漏不是指內(nèi)存泄漏,而是指該傳入?yún)?shù)的內(nèi)容的生命期,超過函數(shù)調(diào)用期,也就是函數(shù)返回后,該參數(shù)的內(nèi)容仍然存活
os.Stdout逃逸到堆上
Fprintln的w、a參數(shù)都泄漏
*pp.free的p參數(shù)(就是receiver)泄漏
該receiver逃逸到heap
ppFree逃逸到heap(這是個全局變量)
把ppFree.Put(p))這行注釋掉(因為可能是它引用了最初傳入的參數(shù)),然后重新go tool compile -m。仍然被移動到堆上。
繼續(xù)往上分析,然后居然發(fā)現(xiàn)這條:
a.go:1272: reflect.t·2 escapes to heap
對應(yīng)的代碼是:
isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
reflect.TypeOf(arg).Kind()居然會導(dǎo)致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()會導(dǎo)致arg逃逸。
按照前面的辦法,復(fù)制reflect包的內(nèi)容到文件里的話,會比較麻煩,因為有些函數(shù)是定義在runtime包里的。
所以只要一些骨架,能重現(xiàn)就行:
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 結(jié)果是:
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在堆上分配了。
(分析一小時后……)
結(jié)論是,調(diào)用interface的方法會導(dǎo)致變量被移到堆上。將上面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會移到堆上。
所以問題變成,為什么調(diào)用接口方法會使引用的變量被放到堆上。
在repo搜索了下,發(fā)現(xiàn)是個known issue: https://github.com/golang/go/issues/7213 ,而且缺少關(guān)愛。
可能轉(zhuǎn)到SSA后端后,會有更好的優(yōu)化吧。
所以現(xiàn)在想優(yōu)化掉這個的話,只能避免使用接口方法了。
|