逃逸分析(Escape analysis)是由编译器决定内存分配位置的一种技术,不需要程序员明确指定。函数中申请一个新对象时,编译器决定该对象是分配在栈中还是堆中:
有了逃逸分析,返回函数局部变量变得可能,这与闭包息息相关。了解哪些场景下对象会逃逸至关重要。
当函数中申请新对象时,编译器会根据该对象是否被函数外部引用来决定是否逃逸:
注意,对于函数外部没有引用的对象,也有可能放到堆中,比如内存过大超过栈的存储能力。
Go语言中可以返回局部变量指针,这是一个典型的变量逃逸案例。示例代码如下:
package main
type Student struct {
Name string
Age int
}
func StudentRegister(name string, age int) *Student {
s := new(Student) // 局部变量s逃逸到堆
s.Name = name
s.Age = age
return s
}
func main() {
StudentRegister("Jim", 18)
}
通过编译参数 -gcflag=-m
可以查看编译过程中的逃逸分析:
# _/D_/SourceCode/GoExpert/src
.\main.go:8: new(Student) escapes to heap
可见在 StudentRegister()
函数中,代码第9行显示“escapes to heap”,表示该行内存分配发生了逃逸现象。
下面的代码 Slice()
函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。
package main
func Slice() {
s := make([]int, 1000, 1000)
for index := range s {
s[index] = index
}
}
func main() {
Slice()
}
查看编译提示:
# _/D_/SourceCode/GoExpert/src
.\main.go:4: Slice make([]int, 1000, 1000) does not escape
当切片长度扩大到10000时,编译提示如下:
# _/D_/SourceCode/GoExpert/src
.\main.go:4: make([]int, 10000, 10000) escapes to heap
当栈空间不足以存放当前对象时或无法判断当前切片长度时,会将对象分配到堆中。
很多函数参数为 interface
类型,比如 fmt.Println(a ...interface{})
,编译期间很难确定其参数的具体类型,也会产生逃逸。如下代码所示:
package main
import "fmt"
func main() {
s := "Escape"
fmt.Println(s)
}
查看编译提示:
# _/D_/SourceCode/GoExpert/src
.\main.go:7: s escapes to heap
某著名的开源框架实现了一个返回Fibonacci数列的函数。该函数返回一个闭包,闭包引用了函数的局部变量a和b,使用时通过该函数获取该闭包,然后每次执行闭包都会依次输出Fibonacci数列。完整的示例程序如下所示:
package main
import "fmt"
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("Fibonacci: %d\n", f())
}
}
Fibonacci() 函数中原本属于局部变量的 a 和 b 由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。
通过了解逃逸分析,可以更好地优化代码并提高程序性能。
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论