Golang原理剖析(逃逸分析)

文章目录

什么是逃逸

逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。

函数的运行都是在栈上面运行的,在栈上面声明临时变量,分配内存,函数运行完毕之后,回收内存,每个函数的栈空间都是独立的,其他函数是无法进行访问,但是在某些情况下栈上面的数据需要在函数结束之后还能被访问,这时候就会设计到内存逃逸了,什么是逃逸,就是抓不住

如果变量从栈上面逃逸,会跑到堆上面 ,栈上面的变量在函数结束的时候回自动回收,回收代价比较小,栈的内存分配和使用一般只需要两个CPU指令"PUSH"和"RELEASE",分配和释放 ,而堆分配内存,则是首先需要找到一块大小合适的内存,之后通过GC回收才能释放,对于这种情况,频繁的使用垃圾回收,则会占用比较大的系统开销,所以尽量分配内存到栈上面,减少gc的压力,提高程序运行速度

逃逸分析过程​

Go语言最基本的逃逸分析原则:如果一个函数返回一个对变量的引用,那么它就会发生逃逸。​

在任何情况下,如果一个值被分配到了栈之外的地方,那么一定是到了堆上面。简而概之:编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上​

Go语言里面没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器是通过分析代码来决定将变量分配到何处。​

简单来说,编译器会根据变量是否被外部引用来决定是否逃逸:​

如果函数外部没有引用,则优先放到栈中;​

如果函数外部存在引用,则必定放到堆中;

指针逃逸

我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

如下示例

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)
}

new 和 make 不一定分配在堆上。

在 Go 里,"在堆上还是栈上"不是由你写了 new/make 决定的,而是由编译器的逃逸分析决定的:只要编译器能证明对象不会在函数返回后继续被引用、也不会以某种方式"跑出当前栈帧",它就可能放在栈上;否则才放到堆上。

new(T):返回 *T,得到"一个零值 T 的地址"。

make:只用于 slice/map/chan,返回"可用的"slice/map/chan(已初始化好内部结构)

逃逸分析 本身是对变量生命周期的判断 ​ 如果变量的生命周期可以随栈帧的结束而释放 ​ 我们就更倾向于将它分配在栈上 ​ 反之,如果编译器无法完全确定这个变量的周期被 控制在栈帧内,就需要分配到堆上

虽然在函数 StudentRegister() 内部 s 为局部变量,其值通过函数返回值返回,s 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。

使用命令 go build -gcflags="-m -l" main.go

go 复制代码
root@GoLang:~/proj/goforjob# go build -gcflags="-m -l" main.go
# command-line-arguments
./main.go:8:22: leaking param: name
./main.go:9:10: new(Student) escapes to heap

"escapes to heap",代表该行内存分配发生了逃逸现象。

栈空间不足

go 复制代码
package main

func MakeSlice() {
	s := make([]int, 100)

	for index := range s {
		s[index] = index
	}
}

func main() {
	MakeSlice()
}

执行

bash 复制代码
go build -gcflags="-m -l" main.go

分析结果:

go 复制代码
root@GoLang:~/proj/goforjob# go build -gcflags="-m -l" main.go
# command-line-arguments
./main.go:4:11: make([]int, 100) does not escape

此时栈空间充足,slice分配在栈上,未发生逃逸,假设将slice扩大100倍,再看一下

go 复制代码
package main

func MakeSlice() {
	s := make([]int, 10000)

	for index := range s {
		s[index] = index
	}
}

func main() {
	MakeSlice()
}
go 复制代码
root@GoLang:~/proj/goforjob# go build -gcflags="-m -l" main.go
# command-line-arguments
./main.go:4:11: make([]int, 10000) escapes to heap

此时,分配的slice容量太大,当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中

动态类型逃逸

很多函数参数为interface类型。比如:

go 复制代码
func Printf(format string, a ...interface{}) (n int, err error)
func Sprintf(format string, a ...interface{}) string
func Fprintf(w io.Writer, a ...interface{}) (n int, err error)
func Print(a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

编译期间很难确定其参数的具体类型,也能产生逃逸。

go 复制代码
package main

import "fmt"

type Student struct {
	Name string
	Age  int
}

func main() {
	s := Student{Name: "Jim", Age: 18}
	fmt.Printf("%v\n", s) // s 作为 interface{} 传入
}
go 复制代码
root@GoLang:~/proj/goforjob# go build -gcflags="-m -l" main.go
# command-line-arguments
./main.go:12:12: ... argument does not escape
./main.go:12:21: s escapes to heap

逃逸常见情况

  1. 指针逃逸,函数内部返回一个局部变量指针

  2. 分配大对象,导致栈空间不足,不得不分配到堆上

  3. 调用接口类型的方法。接口类型的方法调用是动态调度。实际使用的具体实现只能在运行时确定。考虑一个接口类型为 io.Reader 的变量 r。对 r.Read(b) 的调用将导致 r 的值和字节片 b 的后续转义因此分配到堆上。

如何避免

  1. go 中的接口类型的方法调用是动态调度,因此不能够在编译阶段确定,所有类型结构体转换成接口的过程会涉及到内存逃逸的情况发生。如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型

  2. 由于切片一般都是使用在函数传递的场景下,而且切片在 append 的时候可能会涉及到重新分配内存,如果切片在编译期间的大小不能够确认或者大小超出栈的限制,多数情况下都会分配到堆上

总结

  1. 堆上动态分配内存比栈上静态分配内存,开销大很多。

  2. 变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。

  3. Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。

  4. 对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过 go build -gcflags="-m -l" main.go 命令来观察变量逃逸情况就行了

  5. 不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。

参数本身需要存放空间(栈/寄存器等)。如果参数是指针,拷贝通常只拷贝指针值;而指针指向的对象是否在堆上,取决于逃逸分析:当该对象可能在函数返回后仍被引用时才会分配到堆上。栈上存的是指针值;指针指向的对象在哪里取决于逃逸分析/对象来源。

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
上海广测检测科技有限公司7 分钟前
消费类智能联网设备安全分级国标征求意见要点解析:三级五维框架与跨境对标逻辑
网络·经验分享
高频因子挖掘机29 分钟前
只取最近 60 个交易日,却每次拉全历史:量化脚本变慢的原因与改法
后端·github·api
孟健1 小时前
从5000亿Token到83%字节匹配:AI逆向工程与重构的工程闭环实践
后端·架构·ai编程
富唯智能机器人2 小时前
晶振弹匣上下料,复合机器人如何实现柔性自动化|富唯智能
经验分享
码事漫谈2 小时前
丢得静悄悄,谈一谈 Agent 的记忆是怎么没的
后端
迪飞特科技2 小时前
智慧物业平台实战:Spring Boot 整合小程序实现报修巡检费用全流程
spring boot·后端·小程序
考虑考虑2 小时前
@JsonInclude注解使用
spring boot·后端·spring
IT_陈寒3 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信3 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
CCblandy3 小时前
HTTP 长连接 3 类故障实测:[WinError 10053]、99 个 TIME_WAIT、69 次多余握手,4 种对策怎么选
后端·python·网络协议·http·云原生·性能优化·架构