详解Go语言中的逃逸分析

 更新时间:2023年09月15日 10:42:30   作者:similar  
逃逸分析是编译器用于决定将变量分配到栈上还是堆上的一种行为,下面小编就来为大家详细讲讲go语言中是如何进行逃逸分析的,需要的小伙伴可以参考下

什么是逃逸

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

众所周知,函数的运行都在操作系统内存空间中的栈空间内。我们在栈上声明临时变量,分配内存,函数运行完毕后,回收内存。每个函数的栈空间都是独立的,其他函数没有权限访问。但在某些情况下,我们需要在函数结束以后访问栈上面的某些数据,这就涉及到内存逃逸了。

如果变量从栈上逃逸,那么他会逃到哪儿去呢?他会跑到堆上。由于栈上的变量是在函数结束的时候自动进行回收,回收代价比较小;而堆空间分配内存,则首先需要找到一块大小合适的内存,之后通过GC回收才能释放。对于这种情况,频繁使用垃圾回收会占用比较大的开销,所以要尽量分配内存到栈上,减少GC的压力。

逃逸分析基本过程

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

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

不同于C++中的new,Go语言中的new关键字不一定会将内存分配到堆空间上,在Go语言中,没有关键字或者函数可以直接将变量分配到堆上,而是通过编译器来分析代码决定将变量分配到何处。

一句话:

编译器会根据变量是否被外部引用来决定是否逃逸。

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

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

常见逃逸情况

指针逃逸

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

比如:

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("similar", 18)
}

虽然函数StudentRegister内部s为局部变量,但是由于返回了指针,其指向的内存地址不会是栈而是堆,这是典型的逃逸案例。

使用命令 go build -gcflags '-m -l' main.go,得到:

# command-line-arguments
./escape.go:8:22: leaking param: name
./escape.go:9:10: new(Student) escapes to heap # 表示该行内存发生了逃逸现象

栈空间不足

如果分配太大容量的slice在栈上,当栈空间不足存放当前对象或者无法判断当前切片长度时就会将对象分配到堆中。

package main
func MakeSlice() {
	s := make([]int, 10000, 10000)
	for index := range(s){
		s[index] = index
	}
}
func main() {
	MakeSlice()
}

同样使用命令go build -gcflags '-m -l' main.go:

# command-line-arguments
./escape_1.go:4:11: make([]int, 10000) escapes to heap

动态类型逃逸

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

func Printf(format string, a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)

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

变量大小不确定

在创建切片,初始化切片容量的时候,有时会传入一个变量指定其大小,由于变量的值不能被编译器确定,所以不能确定其占用空间大小,从而编译器可能会直接将变量分配到堆上。

package main
func MakeSlice() {
	length := 1
	a := make([]int, length, length)
	for i := 0; i < length; i++ {
		a[i] = i
	}
}
func main() {
	MakeSlice()
}

编译结果:

# command-line-arguments
./escape_1.go:5:11: make([]int, length, length) escapes to heap

常见的逃逸情况总结

指针逃逸:函数内部返回一个局部变量指针

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

调用接口类型的方法,接口类型的方法调用是动态调度 - 实际使用的具体实现只能在运行时确定。

尽管能够符合分配到栈的场景,但是其大小不能在编译的时候确定,也会分配到堆上。

如何避免

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

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

总结

堆上分配内存比栈上分配内存,开销大很多。

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

Go语言编译器会通过变量是否被外部引用类决定是否逃逸

通过go build -gcflags '-m'命令可以观察变量是否逃逸

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

到此这篇关于详解Go语言中的逃逸分析的文章就介绍到这了,更多相关Go逃逸分析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go依赖注入工具wire的具体使用

    Go依赖注入工具wire的具体使用

    本文主要介绍了Go依赖注入工具wire的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • Go设计模式之迭代器模式讲解和代码示例

    Go设计模式之迭代器模式讲解和代码示例

    迭代器是一种行为设计模式, 让你能在不暴露复杂数据结构内部细节的情况下遍历其中所有的元素,本文将为大家详细介绍Go 迭代器模式,文中详细的代码示例,需要的朋友可以参考下
    2023-07-07
  • golang 使用time包获取时间戳与日期格式化操作

    golang 使用time包获取时间戳与日期格式化操作

    这篇文章主要介绍了golang 使用time包获取时间戳与日期格式化操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 几个小技巧帮你实现Golang永久阻塞

    几个小技巧帮你实现Golang永久阻塞

    Go 的运行时的当前设计,假定程序员自己负责检测何时终止一个 goroutine 以及何时终止该程序。有时候我们需要的是使程序阻塞在这一行,本文就来详细的介绍一下,感兴趣的可以了解一下
    2021-12-12
  • 详解Go 并发

    详解Go 并发

    这篇文章主要介绍了Go 并发的相关资料,帮助大家更好的理解和学习go语言,感兴趣的朋友可以了解下
    2020-09-09
  • Go压缩位图库roaring安装使用详解

    Go压缩位图库roaring安装使用详解

    这篇文章主要为大家介绍了Go压缩位图库roaring安装使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Golang巧用defer进行错误处理的方法

    Golang巧用defer进行错误处理的方法

    错误处理是程序的重要组成部分,有效且优雅的处理错误是大多数程序员的追求,下面这篇文章主要给大家介绍了关于Golang中巧用defer进行错误处理的方法,文中通过示例介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2017-05-05
  • Go语言中的Struct结构体

    Go语言中的Struct结构体

    这篇文章介绍了Go语言中的Struct结构体,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • 深入解析Go语言中for循环的写法

    深入解析Go语言中for循环的写法

    这篇文章主要介绍了Go语言中for循环的写法,是Golang入门学习中的基础知识,需要的朋友可以参考下
    2015-10-10
  • golang中defer的使用规则详解

    golang中defer的使用规则详解

    大家应该都知道在golang当中,defer代码块会在函数调用链表中增加一个函数调用。下面这篇文章主要给大家介绍了关于golang中defer的使用规则,文中介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2017-07-07

最新评论