GoLang 逃逸分析的机制详解
作者:帘外五更风 发布时间:2023-08-06 16:46:43
对于手动管理内存的语言,比如 C/C++,调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄露,搞得胆战心惊。
但是 Golang 并不是这样,虽然 Golang 语言里面也有 new。Golang 编译器决定变量应该分配到什么地方时会进行逃逸分析。使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序员“模糊化”了,当然这一切都是Go编译器在背后帮我们完成的。一个变量是在堆上分配,还是在栈上分配,是经过编译器的逃逸分析之后得出的结论。
一、 逃逸分析是什么
wiki定义
In compiler optimization, escape analysis is a method for determining the dynamic scope of pointers - where in the program a pointer can be accessed. It is related to pointer analysis and shape analysis.
When a variable (or an object) is allocated in a subroutine, a pointer to the variable can escape to other threads of execution, or to calling subroutines. If an implementation uses tail call optimization (usually required for functional languages), objects may also be seen as escaping to called subroutines. If a language supports first-class continuations (as do Scheme and Standard ML of New Jersey), portions of the call stack may also escape.
If a subroutine allocates an object and returns a pointer to it, the object can be accessed from undetermined places in the program — the pointer has "escaped". Pointers can also escape if they are stored in global variables or other data structures that, in turn, escape the current procedure.
Escape analysis determines all the places where a pointer can be stored and whether the lifetime of the pointer can be proven to be restricted only to the current procedure and/or threa.
C/C++中,有时为了提高效率,常常将pass-by-value(传值)“升级”成pass-by-reference,企图避免构造函数的运行,并且直接返回一个指针。然而这里隐藏了一个很大的坑:在函数内部定义了一个局部变量,然后返回这个局部变量的地址(指针)。这些局部变量是在栈上分配的(静态内存分配),一旦函数执行完毕,变量占据的内存会被销毁,任何对这个返回值作的动作(如解引用),都将扰乱程序的运行,甚至导致程序直接崩溃。例如:
int *foo ( void )
{
int t = 3;
return &t;
}
为了避免这个坑,有个更聪明的做法:在函数内部使用new函数构造一个变量(动态内存分配),然后返回此变量的地址。因为变量是在堆上创建的,所以函数退出时不会被销毁。但是,这样就行了吗?new出来的对象该在何时何地delete呢?调用者可能会忘记delete或者直接拿返回值传给其他函数,之后就再也不能delete它了,也就是发生了内存泄露。关于这个坑,大家可以去看看《Effective C++》条款21,讲得非常好!
C++是公认的语法最复杂的语言,据说没有人可以完全掌握C++的语法。而这一切在Go语言中就大不相同了。像上面示例的C++代码放到Go里,没有任何问题。
你表面的光鲜,一定是背后有很多人为你撑起的!Go语言里就是编译器的逃逸分析。它是编译器执行静态代码分析后,对内存管理进行的优化和简化。
在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。
更简单来说,逃逸分析决定一个变量是分配在堆上还是分配在栈上。
二、 为什么要逃逸分析
前面讲的C/C++中出现的问题,在Go中作为一个语言特性被大力推崇。真是C/C++之 * Go之蜜糖!
C/C++中动态分配的内存需要我们手动释放,导致猿们平时在写程序时,如履薄冰。这样做有他的好处:程序员可以完全掌控内存。但是缺点也是很多的:经常出现忘记释放内存,导致内存泄露。所以,很多现代语言都加上了垃圾回收机制。
Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。
逃逸分析这种“骚操作”把变量合理地分配到它该去的地方,“找准自己的位置”。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前实现 * 主义!
如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。
堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。
通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。
三、 逃逸分析如何完成
Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。
简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。
Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。
对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。
简单来说,编译器会根据变量是否被外部引用来决定是否逃逸:
1)如果函数外部没有引用,则优先放到栈中;
2) 如果函数外部存在引用,则必定放到堆中;
针对第一条,可能放到堆上的情形:定义了一个很大的数组,需要申请的内存过大,超过了栈的存储能力。
四、 逃逸分析实例
下面是一个简单的例子。
package main
import ()
func foo() *int {
var x int
return &x
}
func bar() int {
x := new(int)
*x = 1
return *x
}
func main() {}
开启逃逸分析日志很简单,只要在编译的时候加上-gcflags '-m',但是我们为了不让编译时自动内连函数,一般会加-l参数,最终为-gcflags '-m -l',执行如下命令:
$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:5:9: &x escapes to heap
./main.go:4:6: moved to heap: x
./main.go:9:10: bar new(int) does not escape
上面代码中foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了栈上。
也可以使用反汇编命令看出变量是否发生逃逸。
$ go tool compile -S main.go
截取部分结果,图中标记出来的说明foo中x是在堆上分配内存,发生了逃逸。
反汇编命令结果
什么时候逃逸呢? golang.org FAQ 上有一个关于变量分配的问题如下:
Q: How do I know whether a variable is allocated on the heap or the stack?
A: From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
关于什么时候逃逸,什么时候不逃逸,我们接下来再看几个小例子。
1)Example1
package main
type S struct{}
func main() {
var x S
y := &x
_ = *identity(y)
}
func identity(z *S) *S {
return z
}
结果如下:
# command-line-arguments
./main.go:8:22: leaking param: z to result ~r1 level=0
./main.go:5:7: main &x does not escape
这里的第一行表示z变量是“流式”,因为identity这个函数仅仅输入一个变量,又将这个变量作为返回输出,但identity并没有引用z,所以这个变量没有逃逸,而x没有被引用,且生命周期也在mian里,x没有逃逸,分配在栈上。
2)Example2
package main
type S struct{}
func main() {
var x S
_ = *ref(x)
}
func ref(z S) *S {
return &z
}
结果如下:
# command-line-arguments
./main.go:8:9: &z escapes to heap
./main.go:7:16: moved to heap: z
这里的z是逃逸了,原因很简单,go都是值传递,ref函数copy了x的值,传给z,返回z的指针,然后在函数外被引用,说明z这个变量在函数內声明,可能会被函数外的其他程序访问。所以z逃逸了,分配在堆上
3)Example3
package main
type S struct {
M *int
}
func main() {
var i int
refStruct(i)
}
func refStruct(y int) (z S) {
z.M = &y
return z
}
结果如下:
# command-line-arguments
./main.go:10:8: &y escapes to heap
./main.go:9:26: moved to heap: y
看日志的输出,这里的y是逃逸了,看来在struct里好像并没有区别,有可能被函数外的程序访问就会逃逸
4)Example4
package main
type S struct {
M *int
}
func main() {
var i int
refStruct(&i)
}
func refStruct(y *int) (z S) {
z.M = y
return z
}
结果如下:
# command-line-arguments
./main.go:9:27: leaking param: y to result z level=0
./main.go:7:12: main &i does not escape
这里的y没有逃逸,分配在栈上,原因和Example1是一样的。
5)Example5
package main
type S struct {
M *int
}
func main() {
var x S
var i int
ref(&i, &x)
}
func ref(y *int, z *S) {
z.M = y
}
结果如下:
# command-line-arguments
./main.go:10:21: leaking param: y
./main.go:10:21: ref z does not escape
./main.go:8:6: &i escapes to heap
./main.go:7:6: moved to heap: i
./main.go:8:10: main &x does not escape
这里的z没有逃逸,而i却逃逸了,这是因为go的逃逸分析不知道z和i的关系,逃逸分析不知道参数y是z的一个成员,所以只能把它分配给堆。
来源:https://studygolang.com/articles/26378
猜你喜欢
- 在网页中放iframe,如果frameborder=0;就没有边框显示了;但动态创建时,在IE7中就不行了,从网上找到解决的办法,写出来记录
- 代码如下: <% dim fso,objFolder,objFiles dim filelist Set fso=Server.Cre
- 来看看javascript怎么实现自动点击超级链接吧,主要使用了js中的onclick事件。这里推荐大家看看这篇文章js鼠标事件大全。看了这
- 以前的Sony Ericsson牌DVD影碟机坏掉了,上周到沃尔玛买了个philips的回来,于是又淘了一些DVD回来看。在使用遥控的时候忽
- HTML 的空白符处理规则HTML 中的“空白符”包括空格 (space)、制表符 (tab)、换行符 (CR/LF) 三种。我们知道,在默
- 需要准备的工具:SQL Query Analyzer和SqlExec Sunx Version第一部分:去掉xp_cmdshell保护系统的
- 日期和时间类型MySQL有多个表示各种日期和时间值的数据类型, 比如YEAR和DATE. MySQL存储时间的最精确粒度是秒。 然而, 能做
- 在做项目的过程中,我们经常会建立各种各样的规范,以方便团队之间更好的合作更好的完成项目;同样我们也经常会听到各种各样的协议,比如Google
- 1.建立设计规范的意义 建立设计文档的根本目的
- 好多同志对 iframe 是如何控制的,并不是十分了解,基本上还处于一个模糊的认识状态.注意两个事项,ifr 是一个以存在的 iframe
- 你还没用 jQuery 写过导航菜单? 相信看到这些出色的jQuery导航菜单后,一定会为此而后悔没早点把 jQuery 应用到自己的Web
- 因为要写个东西用到,所以百度了一下,居然有朋友乱写,而且比较多,都没有认真测试过,只对字符可以,但是对数字就不可以,而且通用性很差,需要修改
- 1.网页背景色的设置 犯错机率:很大普遍性:较广犯错可能性:懒/不知道约2年前我曾发现21cn上出现过一次没有设置背景色的情况,当时我用Em
- 你说的就是真正的计数器,它只在有新的用户进入网站时,计数器才会加1,忠实可靠。把下列代码放到的global.asa的sessio
- 原文地址:30 Days of Mootools 1.2 Tutorials - Day 4 - Functions函数和MooTools
- 今天碰到这个极度郁闷的报错,搞了大半下午,才发现是ie的问题,忍不住大骂。例子是这样的:页面中有多处能出发菜单,并且菜单出现在触发点的旁边,
- 在使用SQL Server 的过程中,由于经常需要从多个不同地点将数据集中起来或向多个地点复制数据,所以数据的导出,导入是极为常见的操作.我
- 抓取“xmly”鬼故事音频import json # 在这个url,音频链接为JSON动态生成,所以用到了json模块impor
- android开发中在和服务器端接口对接时出现编码问题,从服务器端获取到的数据是 "\u8bbe\u59071ID-\u
- 先来看看js中的Null类型表示什么?null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象,一般一个未定义的变量在初次使用