go逃逸分析(是什么?作用、场景示例)

152 阅读4分钟

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 大切片创建

空间限制:

  1. 大切片的空间比较大,当切片超出了栈空间的大小,编译器就会分配在堆上
  2. 避免频繁分配:栈空间大小有限,虽然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 运行时会移动内存(如栈深拷贝),指针指向地址可能变化