避免逃逸的好处
- 最大的好处应该是减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
- 因为逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好
- 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
分析工具
压测命令:go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
编译命令:go build -gcflags "-N -m -m -l"
解释:
- -gcflags
-N 禁止编译优化
-l 禁止内联,禁止内联也可以一定程度上减小可执行程序大小
-m 逃逸分析 , 可以重复最多四次
-benchmem 压测时打印内存分配统计
go版本:1.12.1
不同版本的gc处理机制可能会不同
接下来根据逃逸类型来逐个进行演示
案例分析
- 取址符号&并return,会产生逃逸
package main
type UserData struct {
Name string
}
func main() {
var info UserData
info.Name = "WilburXu"
_ = GetUserInfo(info)
}
func GetUserInfo(userInfo UserData) *UserData {
return &userInfo //发生引用,产生逃逸逃逸
}
修正:
func main() {
var info UserData
info.Name = "WilburXu"
_ = GetUserInfo(&info)
}
func GetUserInfo(userInfo *UserData) *UserData {
return userInfo //相当于值传递,只不过是传递的地址的拷贝,指向了同一个目标
}
原因:GetUserInfo里面发生了引用, 去掉引用后变量 userInfo 变成了 leaking param,即流式变量,由于该变量仅在子函数中使用,随着函数退出自动销毁
- 指针间接赋值 (star-dot-equals)
package main
import (
"testing"
)
func BenchmarkAssignmentIndirect(b *testing.B) {
type X struct {
p *int
}
for i := 0; i < b.N; i++ {
var i1 int
x1 := &X{
p: &i1, //结构体内部赋值,不逃逸
}
_ = x1
var i2 int
x2 := &X{}
x2.p = &i2 // 结构体外部.操作赋值,属间接调用,逃逸
}
}
[developer@localhost test]$ go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
# test [test.test]
./main_test.go:20:10: &i2 escapes to heap
./main_test.go:20:10: from x2.p (star-dot-equals) at ./main_test.go:20:8
./main_test.go:18:7: moved to heap: i2
./main_test.go:7:34: BenchmarkAssignmentIndirect b does not escape
./main_test.go:14:7: BenchmarkAssignmentIndirect &i1 does not escape
./main_test.go:14:4: BenchmarkAssignmentIndirect &X literal does not escape
./main_test.go:19:9: BenchmarkAssignmentIndirect &X literal does not escape
注:如果 “.操作的变量”不是指针,则不会发生逃逸
- interface{}类型赋值:( passed to call[argument escapes])
package main
type User struct {
name string
}
func main() {
var a int
interfaceA(&a)
name := "hello world"
MyPrintln(name) //name 逃逸, 通过interface传递调用赋值
}
func MyPrintln(one interface{}) (n int, err error) {
userInfo := new(User)
userInfo.name = one.(string) //结构体(复杂类型),间接赋值发生逃逸
_ = userInfo
return
}
func interfaceA(a interface{}){
var c interface{}
c = a.(int) //简单变量不逃逸
_ = c //防止编译错误
}
[developer@localhost test]$ go build -gcflags "-N -m -m -l"
# test
./main.go:18:17: interfaceA a does not escape
./main.go:20:4: interfaceA a.(int) does not escape
./main.go:12:16: leaking param: one
./main.go:12:16: from userInfo.name (star-dot-equals) at ./main.go:14:16
./main.go:13:17: MyPrintln new(User) does not escape
./main.go:10:11: name escapes to heap
./main.go:10:11: from name (passed to call[argument escapes]) at ./main.go:10:11
./main.go:7:13: main &a does not escape
./main.go:7:13: main &a does not escape
修正:userInfo 直接赋值
package main
type User struct {
name string
}
func main() {
name := "hello world"
MyPrintln(name)
}
func MyPrintln(one interface{}) (n int, err error) {
//直接赋值
userInfo := &User{
name : one.(string),
}
_ = userInfo
return
}
[developer@localhost test]$ go build -gcflags "-N -m -m -l"
# test
./main.go:9:16: MyPrintln one does not escape
./main.go:12:3: MyPrintln &User literal does not escape
./main.go:7:11: main name does not escape
- 间接调用 (parameter to indirect call)
eg:1
func BenchmarkLiteralFunctions(b *testing.B) {
for i := 0; i < b.N; i++ {
var y1 int
foo(&y1, 42)
var y2 int
func(p *int, x int) {
*p = x
}(&y2, 42)
var y3 int
p := foo
p(&y3, 42) // 变量y3逃逸
}
}
func foo(p *int, x int) {
*p = x
}
func BenchmarkHandler(b *testing.B) {
// Setup route with specific handler.
h := func(w http.ResponseWriter, r *http.Request) error {
// fmt.Println("Specific Request Handler")
return nil
}
route := wrapHandler(h)
// Execute route.
for i := 0; i < b.N; i++ {
var r http.Request
route(nil, &r) //r发生逃逸,间接调用h函数,
//修正方法:用h 直接替换route函数
}
}
type Handler func(w http.ResponseWriter, r *http.Request) error
func wrapHandler(h Handler) Handler {
f := func(w http.ResponseWriter, r *http.Request) error {
return h(w, r)
}
return f
}
- map & slice 指针赋值 (value of map put , slice-element-equals)
package main
import (
"testing"
)
func BenchmarkSliceMapAssignment(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]*int)
var x1 int
m[0] = &x1 //x1逃逸
s := make([]*int, 1)
var x2 int
s[0] = &x2 //x2逃逸
}
}
[developer@localhost test]$ go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
# test [test.test]
./main_test.go:10:10: &x1 escapes to heap
./main_test.go:10:10: from m[0] (value of map put) at ./main_test.go:10:8
./main_test.go:9:7: moved to heap: x1
./main_test.go:14:10: &x2 escapes to heap
./main_test.go:14:10: from s[0] (slice-element-equals) at ./main_test.go:14:8
./main_test.go:13:7: moved to heap: x2
./main_test.go:6:34: BenchmarkSliceMapAssignment b does not escape
./main_test.go:8:12: BenchmarkSliceMapAssignment make(map[int]*int) does not escape
./main_test.go:12:12: BenchmarkSliceMapAssignment make([]*int, 1) does not escape
修正:map[int]int =》 map[int]int; make([]int, 1) =》 make([]int, 1)**
package main
import (
"testing"
)
type S struct {
I int
}
type myMap map[string]S
type myMapPtr map[string]*S
func BenchmarkValueType(b *testing.B) {
for i := 0; i < b.N; i++ {
mm := make(myMap)
mm["sam"] = S{I:10}
mm.update("sam")
}
}
func BenchmarkPtrType(b *testing.B) {
for i := 0; i < b.N; i++ {
mm := make(myMapPtr)
mm["sam"] = &S{I:10} //逃逸
mm.update("sam")
}
}
func (s myMap) update(key string){
sss := s[key]
sss.I = 100
s[key] = sss
}
func (s myMapPtr) update(key string){
s[key].I = 100
}
[developer@localhost test]$ go test -gcflags "-m -m -l" -run none -bench . -benchmem -memprofile mem.out
# test [test.test]
./main_test.go:26:23: leaking param: key
./main_test.go:26:23: from s[key] (key of map put) at ./main_test.go:29:3
./main_test.go:26:7: myMap.update s does not escape
./main_test.go:12:25: BenchmarkValueType b does not escape
./main_test.go:14:13: BenchmarkValueType make(myMap) does not escape
./main_test.go:31:7: myMapPtr.update s does not escape
./main_test.go:31:26: myMapPtr.update key does not escape
./main_test.go:22:18: &S literal escapes to heap
./main_test.go:22:18: from mm["sam"] (value of map put) at ./main_test.go:22:13
./main_test.go:19:23: BenchmarkPtrType b does not escape
./main_test.go:21:13: BenchmarkPtrType make(myMapPtr) does not escape
./main_test.go:26:23: leaking param: key
./main_test.go:26:23: from key (passed to call[argument escapes]) at :1
:1: (*myMap).update .this does not escape
:1: (*myMapPtr).update .this does not escape
./main_test.go:31:26: (*myMapPtr).update key does not escape
# test.test
/tmp/go-build766679195/b001/_testmain.go:42:42: testdeps.TestDeps literal escapes to heap
/tmp/go-build766679195/b001/_testmain.go:42:42: from testdeps.TestDeps literal (passed to call[argument escapes]) at $WORK/b001/_testmain.go:42:24
goos: linux
goarch: amd64
pkg: test
BenchmarkValueType 20000000 100 ns/op 0 B/op 0 allocs/op
BenchmarkPtrType 20000000 89.6 ns/op 8 B/op 1 allocs/op
PASS
ok test 4.003s
数逃逸分析报告可以直观看出,指针类型的效率略高(我用切片测试反而值类型效率更高,有兴趣的可以测试下),但由于逃逸到堆上分配,废弃时产生垃圾,所以在使用map 和切片时,如果变量只是局部的变量,则尽量用值类型存储,否则频繁创建则会给gc造成压力。
- 接收者间接调用 (receiver in indirect call)
type Iface interface {
Method()
}
type X struct {
name string
}
func (x X) Method() {}
func BenchmarkInterfaces(b *testing.B) {
for i := 0; i < b.N; i++ {
x1 := X{"bill"}
var i1 Iface = x1
var i2 Iface = &x1
i1.Method() //间接调用,因为中间设计到一次接口转换 receiver in indirect call
i2.Method() //...
x2 := X{"bill"}
foo(x2) //间接传递变量 passed to call[argument escapes]
foo(&x2) //间接传递变量 passed to call[argument escapes]
}
}
func foo(i Iface) {
i.Method()
}
即通过接口调用会产生逃逸
总结:
- 局部函数需要返回参数的,如果确定返回后也是局部变量,则避免逃逸的方法是通过入参的形式传递
- 慎用指针
- 空接口interface{} 虽然提高了灵活性,但是变量传递会发生逃逸
- map & slice 初始化时,预估容量,避免由扩展导致的内存分配。但是如果太大(10000)也会逃逸,因为栈的空间是有限的
最后,在写业务代码时,对有性能要求的功能一定要写单元测试,这样可以直接限定我们逃逸分析的区域,避免产生大量不必要的log日志影响查看。
参考:https://studygolang.com/articles/12396?fr=sidebar