Go逃逸分析
1.逃逸分析是什么?
逃逸分析就是编译器去分析一个变量,在被多处引用的时候,分配到栈上,还是堆上。其实就是编译期间,分析代码,如果变量到堆上,那就是逃逸了。
2. 逃逸分析的作用、目的
之所以要进行逃逸分析,是因为栈和堆在内存分配的性能上,存在显著差异。栈对象在函数释放时就随函数销毁了,不需要定时探活,因此操作栈对象要比堆对象更快。而堆内存的分配和释放则相对复杂得多,需要进行GC(内存的查找、碎片化处理)等操作,开销较大。
因此编译器的目的,就是要提高程序运行的速度。就需要把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻堆内存分配的开销,同时也会减少垃圾回收(Garbage Collection,GC)的压力。
3. 逃逸分析的场景示例
可以通过go build -gcflags="-m -l" main.go 启用逃逸分析,查看golang在命令行的逃逸分析结果。
逃逸分析的原则:如果一个函数返回对一个变量的引用,那么这个变量就会发生逃逸。
3.1 函数返回局部变量的指针
当返回局部变量的指针,会从栈逃逸到堆上。因为函数结束后,栈会被销毁,但变量仍然需要被外部代码使用,因此编译器会把变量分配到堆上。
package main
import "fmt"
func CreateX()*int{
x := 4
return &x
}
func main(){
x := CreateX()
fmt.Println(x)
}
运行结果:
go run -gcflags="-m -l" main.go
# command-line-arguments
./main.go:6:2: moved to heap: x
./main.go:12:13: ... argument does not escape
0xc000098010
3.2 切片动态扩容
当切片在运行时动态扩容,编译器不知道到底要分配多少内存,所以切片本身发生了逃逸
package main
import "fmt"
func main(){
s := make([]int, 0)
for i:=0;i<3;i++{
s = append(s, 10)
}
}
运行结果:
./main.go:6:11: make([]int, 0) escapes to heap
./main.go:10:13: ... argument does not escape
./main.go:10:14: s escapes to heap
3.3 大切片创建
空间限制:
- 大切片的空间比较大,当切片超出了栈空间的大小,编译器就会分配在堆上
- 避免频繁分配:栈空间大小有限,虽然gorutine本身的栈空间可以扩容,但终究存在上限。为了避免频繁分配,并且最终移动到堆上,编译器会在大切片创建时,直接移动到堆上。
package main
import "fmt"
func main() {
s := make([]int, 10000)
fmt.Println(len(s))
}
输出结果:
./main.go:6:11: make([]int, 10000) escapes to heap
./main.go:8:13: ... argument does not escape
./main.go:8:17: len(s) escapes to heap
3.4 全局变量(闭包引用)
代码:
package main
func Closure() func() int {
x := 0
return func() int {
x++
return x
}
}
func main() {
Closure()
Closure()
}
输出结果:
./main.go:4:2: moved to heap: x
./main.go:5:9: func literal escapes to heap
3.5 变量作为接口类型传递使用
package main
import "fmt"
func main() {
x := 0
fmt.Println(x)
}
输出结果:
./main.go:7:13: ... argument does not escape
./main.go:7:14: x escapes to heap
4. C++和golang的堆栈是一个东西吗?它们的对比
| 对比维度 | C/C++ | Go |
|---|---|---|
| 操作系统级别的堆和栈 - 栈 | 栈空间是1MB大小,用的是操作系统给每个线程分配的栈,由操作系统和编译器共同管理。 | 栈内存最大是1GB,它其实是操作系统的堆内存,是Go在运行时从堆内存中分配的虚拟栈空间,也是动态的(按需分配、可增长收缩)。栈上可以减轻GC压力。 |
| 操作系统级别的堆和栈 - 堆 | 较大的内存区域,用于动态内存分配。需通过系统调用请求内存,使用后手动释放(如 malloc 和 free) | 用于动态内存分配,使用自动垃圾回收机制,无需手动释放内存 |
| 运行时的内存管理 | 依赖手动内存管理,程序员明确分配和释放内存,灵活性高但易导致内存泄漏等错误 | 采用 GC 垃圾回收机制,程序员无需手动管理内存释放,更简单安全,但有一定运行时开销 |
| 栈内存的深拷贝 | 栈大小固定,通常不涉及栈内存的拷贝操作 | Go 运行时可能将栈深拷贝到更大内存区域以防止碎片化,对程序员透明,但影响指针使用,指针算术运算可能在栈移动后失效 |
| 指针运算 | 指针运算是常见操作,内存地址通常固定,指针算术运算可预测 | 不支持指针算术运算,因为 Go 运行时会移动内存(如栈深拷贝),指针指向地址可能变化 |