fmt.Printf等函数会导致传进去的参数在编译时从栈逃逸到堆上?
golang的issue:(https://github.com/golang/go/issues/8618)
分析工具:
go build -gcflags '-m -l' main.go
)go tool compile -S main.go
其中 编译参数(-gcflags)
介绍:
-N
: 禁止编译优化
-l
: 禁止内联(可以有效减少程序大小)
-m
: 逃逸分析(最多可重复四次)
-benchmem
: 压测时打印内存分配统计
Example:
package main
import (
"fmt"
"runtime"
)
type obj struct{
}
func main() {
a := &obj{
}
fmt.Printf("%p\n", a)
b := &obj{
}
println(b)
}
逃逸分析:
./main.go:17:7: &obj literal escapes to heap
./main.go:18:12: ... argument does not escape
./main.go:20:7: &obj literal does not escape
0x11a6c10
0xc000072f1f
可以看到我们的变量a因为fmt的函数逃逸到了堆上。
首先声明,我们只是验证了这个问题的发生,但并没有解决这个问题,有想法的同学可以直接去提交m
目前网上的解释有2个方向:
fmt.Printf等函数会导致传进去的参数在编译时从栈逃逸到堆上
其实,fmt.Printf
的第二个参数,是一个 interface 类型,在底层的调用中用到了断言,具体的调用逻辑是:
Printf->Fprintf->doPrintf->reflect.TypeOf(arg).Kind()
这里有人通过模拟fmt包得出了初步的结论(https://reusee.github.io/post/escape_analysis/),调用interface的Type方法会导致变量被移到堆上。
所以我们可以认为a在编译阶段,编译器无法确定其具体的类型。因此会产生逃逸,最终分配到堆上(最本质的原因是interface{}类型一般情况下底层会进行reflect
,而使用的reflect.TypeOf(arg).Kind()
获取接口类型对象的底层数据类型时发生了堆逃逸,最终就会反映为当入参是空接口类型时发生了逃逸)。
但不是说往func(interface{})
传值,或者往func(*struct)
传指针就会导致逃逸分析。只是大多数场景下,其内部都会用到反射,导致逃逸(switch type不会导致逃逸)。
验证:
package main
import (
"fmt"
"runtime"
)
type obj struct{
}
func main() {
a := &obj{
}
fmt.Printf("%p\n", a)
b := &obj{
}
reflect.TypeOf(b).Kind()
println(b)
}
逃逸分析:
# command-line-arguments
./main.go:20:7: &obj literal escapes to heap
./main.go:21:12: ... argument does not escape
./main.go:23:7: &obj literal escapes to heap
0x11a6c30
0x11a6c30
可以发现两个变量都到了堆上,至于地址为什么一摸一样,可以关注我的另一篇文章:https://www.jianshu.com/p/e0fd84a59088
我们点进去看看fmt.Printf的源码,同样的排查链路,Printf->Fprintf->doPrintf->printArg
我们发现有这么一段赋值代码,我们传入的u被赋值给了pp指针的一个成员变量:
func (p *pp) printArg(arg interface{
}, verb rune) {
p.arg = arg
p.value = reflect.Value{
}
...
}
而这个pp类型的指针p是由构造函数newPrinter返回的,所以他的生命周期就变了,p一定发生逃逸,而p引用了传入指针u,经测试是逃逸了。
验证
package main
import (
"fmt"
)
type obj struct{
}
type pointer struct {
o *obj
}
func main() {
a := &obj{
}
fmt.Printf("%p\n", a)
b := &obj{
}
p := newPrinter()
p.o = b
println(b)
}
func newPrinter() *pointer {
return new(pointer)
}
结论:
# command-line-arguments
./main.go:23:12: new(pointer) escapes to heap
./main.go:14:7: &obj literal escapes to heap // a
./main.go:15:12: ... argument does not escape
./main.go:16:7: &obj literal escapes to heap // b
0x11a6c10
0x11a6c10
我们看到被p引用的b也被赶到了堆上
fmt.Printf等函数传入参数会发生堆逃逸。
虽然日常的开发,go已经帮我们处理了编译前的内存分配,我们也不需要关注堆栈的使用情况,但有意识的避免堆逃逸可以有效的提高负担重的服务性能。堆逃逸在go中并不罕见,并且对gc的影响带来的性能消耗也是不容小觑的。
1.函数返回指向栈内对象的指针,或者说是参数泄漏,延长了指针对象的生命周期。
2.调用反射(未知类型)(fmt案例的第一个问题)。
3.被已经逃逸的变量引用的指针,一定发生逃逸(fmt案例的第二个问题)。
4.被指针类型的slice、map和chan引用的指针,一定发生逃逸。
1.减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除
2.逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(系统开销少)
3.减少动态分配所造成的内存碎片
函数传递指针真的比传值效率高吗?
我们知道传递指针可以减少底层值的拷贝,可以提高效率,负担也比较小
但是当数据比较小且多的情况,由于指针传递经常会导致逃逸到堆上,会增加GC的负担,所以传递指针不一定是高效的。
example:
使用指针的chan比使用值的chan慢30%,使用指针的chan发生逃逸,gc拖慢了速度。
ps :https://stackoverflow.com/questions/41178729/why-passing-pointers-to-channel-is-slower