mainGetUserInfo&User{...}
package main
type User struct {
ID int64
Name string
}
func GetUserInfo() *User {
return &User{ID: 1, Name: "niuben"}
}
func main() {
_ = GetUserInfo()
}
什么是堆、栈
堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多。
栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上。
Go 语言,它的堆栈分配是通过 Compiler 进行分析,GC 去管理的,而对其的分析选择动作就是今天探讨的重点。
什么是逃逸分析
在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。
通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:
- 是否有在其他地方(非局部)被引用。只有有可能被引用了,那么它一定分配到堆上。否则分配到栈上。
- 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上。
对此可以理解为,逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。
在什么阶段确定逃逸
在编译阶段确定逃逸,注意并不是在运行时。
为什么需要逃逸
这个问题我们可以反过来想,如果变量都分配到堆上了会出现什么事情?例如:
- 垃圾回收(GC)的压力不断增大。
- 申请、分配、回收内存的系统开销增大(相对于栈)。
- 动态分配产生一定量的内存碎片。
简单来说,就是频繁申请并分配堆内存是有一定“代价”的。会影响应用程序运行的效率,间接影响到整体系统。
因此,“按需分配”最大限度的灵活利用资源,才是正确的治理之道。这也就是为什么需要逃逸分析的原因。
怎么确定是否逃逸
-gcflags
-m-m-l
$ go build -gcflags '-m -l' main.go
第二,通过反编译命令查看
$ go tool compile -S main.go
go tool compile -help
逃逸案例
案例一:指针
第一个案例是一开始抛出的问题,现在你再想想看,如下:
package main
type User struct {
ID int64
Name string
}
func GetUserInfo() *User {
return &User{ID: 1, Name: "niuben"}
}
func main() {
_ = GetUserInfo()
}
执行命令观察一下,如下:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:54: &User literal escapes to heap
&User
$ go tool compile -S main.go
"".GetUserInfo STEXT size=190 args=0x8 locals=0x18
0x0000 00000 (main.go:9) TEXT "".GetUserInfo(SB), $24-8
...
0x0013 00019 (test10.go:8) MOVQ BP, 16(SP)
0x0018 00024 (test10.go:8) LEAQ 16(SP), BP
0x001d 00029 (test10.go:8) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x001d 00029 (test10.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (test10.go:9) LEAQ type."".User(SB), AX
0x0024 00036 (test10.go:9) MOVQ AX, (SP)
0x0028 00040 (test10.go:9) PCDATA $1, $0
0x0028 00040 (test10.go:9) CALL runtime.newobject(SB)
0x002d 00045 (test10.go:9) MOVQ 8(SP), AX
0x0032 00050 (test10.go:9) MOVQ $1, (AX)
0x0039 00057 (test10.go:9) MOVQ $6, 16(AX)
0x0041 00065 (test10.go:9) LEAQ go.string."niuben"(SB), CX
...
runtime.newobject
分析结果
GetUserInfo()
func main() {
str := new(string)
*str = "niuben"
你想想这个对象会分配到哪里?如下:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:4:12: main new(string) does not escape
main
案例二:未确定类型
func main() {
str := new(string)
*str = "niuben"
fmt.Println(str)
}
执行命令观察一下,如下:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:13: str escapes to heap
./main.go:6:12: new(string) escapes to heap
./main.go:9:13: main ... argument does not escape
strfmt
分析结果
fmt.Println(str)
func Println(a ...interface{})(n int, err error)
interface
reflect.TypeOf(arg).Kind()interface
案例三:泄露参数
package main
type User struct {
ID int64
Name string
}
func GetUserInfo(u *User) *User {
return u
}
func main() {
_ = GetUserInfo(&User{ID: 1, Name: "niuben"})
}
执行命令观察一下,如下:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:18: leaking param: u to result ~r1 level=0
./main.go:14:63: main &User literal does not escape
leaking paramuGetUserInfo
main()
再想想
那你再想想怎样才能让它分配到堆上?结合案例一,举一反三。
package main
type User struct {
ID int64
Name string
}
func GetUserInfo(u User) *User {
return &u
}
func main() {
_ = GetUserInfo(User{ID: 1, Name: "niuben"})
}
执行命令观察一下,如下:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:9: &u escapes to heap
./main.go:9:18: moved to heap: u
只要一小改,它就考虑会被外部所引用,因此妥妥的分配到堆上了
总结
在本文我给你介绍了逃逸分析的概念和规则,并列举了一些例子加深理解。但实际肯定远远不止这些案例,你需要做到的是掌握方法,遇到再看就好了。除此之外你还需要注意:
- 静态分配到栈上,性能一定比动态分配到堆上好。
- 底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心。
- 每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)。
- 直接通过 go build -gcflags '-m -l' 就可以看到逃逸分析的过程和结果。
- 到处都用指针传递并不一定是***的,要用对。
这块的知识点。我的建议是适当了解,但没必要硬记,因为 Go 语言每次升级都有可能会改。靠基础知识点加命令调试观察就好了。
参考